Introduction
This document is the formal specification for the Forge programming language, version 0.3.3.
Forge is an internet-native, general-purpose programming language implemented in Rust. It is designed for application-layer work: web services, scripts, data pipelines, prototypes, and tooling. Forge ships with built-in support for HTTP clients and servers, databases (SQLite and PostgreSQL), cryptography, JSON, CSV, terminal UI, AI/LLM integration, and more — eliminating the need for third-party packages for common internet-oriented tasks.
Design Philosophy
Forge is guided by three core principles:
-
Internet-native. The operations developers perform most frequently — HTTP requests, database queries, JSON parsing, cryptographic hashing — are built into the language and its standard library. A REST API server is four lines. A database query is two.
-
Dual syntax. Every construct in Forge has two spellings: a classic syntax familiar to developers who have written JavaScript, Python, Rust, or Go; and a natural-language syntax that reads like English prose. Both forms compile to identical internal representations and may be mixed freely within the same source file.
-
Batteries included. Forge ships 16 standard library modules with over 230 functions, 30 interactive tutorials, a built-in test runner, a formatter, an LSP server, and a project scaffolding tool. A single
cargo installprovides a complete development environment.
Language Overview
Forge is dynamically typed at runtime with optional type annotations (gradual typing). It supports first-class functions, closures, algebraic data types, pattern matching, Result/Option error handling, structural interfaces, composition via delegation, async/await concurrency, and channels. Programs are executed top-to-bottom without requiring a main function.
The default execution engine is a tree-walking interpreter. A bytecode VM (--vm) and a JIT compiler (--jit) are available for performance-critical workloads but support fewer features.
How to Read This Specification
This specification is organized into five parts:
- Part I: Language Core covers lexical structure, types, expressions, statements, the type system, error handling, and concurrency.
- Part II: Standard Library documents each of the 16 built-in modules.
- Part III: Built-in Functions catalogs all globally available functions.
- Part IV: Dual Syntax Reference provides the complete mapping between classic and natural syntax forms.
- Part V: Runtime and Internals describes the interpreter, bytecode VM, JIT compiler, and HTTP server runtime.
Grammar rules are presented in block quotes using an informal EBNF notation. Code examples use forge syntax highlighting. Where both classic and natural syntax exist for a construct, both forms are shown.
Notation Conventions
Throughout this specification:
monospacetext in prose refers to keywords, operators, or identifiers.- Code blocks labeled
forgecontain valid Forge source code. - Grammar productions use
→for derivation,|for alternatives,[ ]for optional elements, and{ }for zero-or-more repetition. - The term “implementation-defined” means the behavior is determined by the specific execution engine (interpreter, VM, or JIT).
Links
- Source code: https://github.com/humancto/forge-lang
- Website and book: https://humancto.github.io/forge-lang/
Credits
Forge was created by Archith Rapaka / HumanCTO.
Lexical Structure
This chapter defines the lexical grammar of Forge: how source text is decomposed into a sequence of tokens. The lexer (tokenizer) reads UTF-8 encoded source text and produces a flat stream of tokens that the parser consumes.
Overview
A Forge source file is a sequence of Unicode characters encoded as UTF-8. The lexer processes this character stream left-to-right, greedily matching the longest valid token at each position. The resulting token stream consists of:
- Keywords — reserved words with special meaning (e.g.,
let,fn,set,define) - Identifiers — user-defined names for variables, functions, types, and fields
- Literals — integer, float, string, boolean, and null values
- Operators — arithmetic, comparison, logical, assignment, and special operators
- Punctuation — delimiters and separators (
(,),{,},[,],,,:,;) - Comments — line comments and block comments, discarded during tokenization
- Newlines — significant for statement termination
- Decorators —
@prefixed annotations
Whitespace and Line Termination
Spaces (U+0020) and horizontal tabs (U+0009) are whitespace characters. They separate tokens but are otherwise insignificant and are not included in the token stream.
Newline characters (U+000A, and the sequence U+000D U+000A) serve as statement terminators. Unlike whitespace, newlines are emitted as Newline tokens because Forge uses newlines (rather than semicolons) to separate statements. Semicolons (;) are recognized as explicit statement terminators but are not required.
Tokenization Order
When the lexer encounters a character sequence, it applies the following precedence:
- Skip whitespace (spaces and tabs).
- If the character begins a comment (
//or/*), consume the entire comment. - If the character is a newline, emit a
Newlinetoken. - If the character is a digit, lex a numeric literal (integer or float).
- If the character is
", lex a string literal (or"""for raw strings). - If the character is a letter or underscore, lex an identifier or keyword.
- Otherwise, lex an operator or punctuation token.
Each token carries a span consisting of the line number, column number, byte offset, and byte length within the source text.
Subsections
The following subsections define each lexical element in detail:
- Source Text — encoding, file extension, line endings
- Keywords — complete keyword list with dual-syntax mappings
- Identifiers — naming rules and special identifiers
- Literals — numeric, string, boolean, null, array, and object literals
- Operators and Punctuation — all operator and delimiter tokens
- Comments — line and block comment syntax
Source Text
Encoding
Forge source files must be encoded in UTF-8. The lexer assumes UTF-8 input and will produce errors on invalid byte sequences. No byte-order mark (BOM) is required or expected; if present, it is treated as ordinary content and will likely cause a parse error.
File Extension
Forge source files use the .fg file extension by convention. The CLI tools (forge run, forge test, forge fmt) expect this extension. Example:
hello.fg
server.fg
tests/math_test.fg
Line Endings
Forge recognizes two line ending sequences:
| Sequence | Name | Unicode |
|---|---|---|
\n | Line feed | U+000A |
\r\n | Carriage return + line feed | U+000D U+000A |
A bare carriage return (\r without a following \n) is not treated as a line ending. Both recognized forms are normalized to a single Newline token in the token stream.
Source Structure
A Forge source file consists of a sequence of top-level statements executed in order. There is no required main function, module declaration, or package header. The simplest valid Forge program is:
say "hello"
Forge programs are executed from the first statement to the last, top to bottom. Functions and type definitions are hoisted conceptually in that they can be referenced before their textual position, but side effects in top-level statements execute in source order.
Character Set
Within string literals, Forge supports the full Unicode character set. Outside of string literals, the following characters are meaningful to the lexer:
- ASCII letters (
a-z,A-Z) and underscore (_) begin identifiers and keywords. - ASCII digits (
0-9) begin numeric literals. - Operator and punctuation characters:
+,-,*,/,%,=,!,<,>,&,|,.,,,:,;,(,),{,},[,],@,?,#. - The double-quote character (
") begins string literals. - Whitespace characters: space (U+0020), horizontal tab (U+0009).
- Line terminators: line feed (U+000A), carriage return (U+000D).
All other characters outside of string literals are lexer errors.
Keywords
Keywords are reserved identifiers with special syntactic meaning. A keyword cannot be used as a variable name, function name, or type name.
Forge’s keyword set is organized into five categories: classic keywords (familiar from other languages), natural-language keywords (Forge’s English-like alternatives), innovation keywords (unique to Forge), error handling keywords, and type keywords.
Classic Keywords
These keywords provide syntax familiar to developers coming from Rust, JavaScript, Go, or Python.
| Keyword | Purpose |
|---|---|
let | Variable declaration |
mut | Mutable variable modifier |
fn | Function declaration |
return | Return from function |
if | Conditional branch |
else | Alternative branch |
match | Pattern matching |
for | For loop |
in | Iterator binding (used with for) |
while | While loop |
loop | Infinite loop |
break | Exit a loop |
continue | Skip to next loop iteration |
struct | Struct type definition |
type | Algebraic data type definition |
interface | Interface definition |
impl | Method block / interface implementation |
pub | Public visibility modifier |
import | Module import |
spawn | Spawn a concurrent task |
true | Boolean literal true |
false | Boolean literal false |
null | Null literal |
async | Async function declaration |
await | Await an async expression |
yield | Yield a value from a generator |
Natural-Language Keywords
These keywords provide English-like alternatives to classic syntax. Each natural keyword maps to an equivalent classic construct.
| Natural Keyword | Classic Equivalent | Usage |
|---|---|---|
set | let | set x to 5 |
to | = | Used with set and change |
change | (reassignment) | change x to 10 |
define | fn | define greet(name) { } |
otherwise | else | } otherwise { } |
nah | else | } nah { } (informal) |
each | (loop modifier) | for each x in items { } |
repeat | (counted loop) | repeat 5 times { } |
times | (loop count) | Used with repeat |
grab | (fetch) | grab resp from "url" |
from | (source) | Used with grab and unpack |
wait | (sleep) | wait 2 seconds |
seconds | (time unit) | Used with wait and timeout |
say | println | say "hello" |
yell | (uppercase print) | yell "hello" prints HELLO |
whisper | (lowercase print) | whisper "HELLO" prints hello |
thing | struct | thing Person { } |
power | interface | power Describable { } |
give | impl | give Person { } |
craft | (constructor) | craft Person { name: "A" } |
the | (connector) | give X the power Y { } |
forge | async | forge fetch_data() { } |
hold | await | hold expr |
emit | yield | emit value |
unpack | (destructure) | unpack {a, b} from obj |
Dual Syntax Mapping
The following table shows equivalent forms for the most common constructs:
| Construct | Classic | Natural |
|---|---|---|
| Variable | let x = 5 | set x to 5 |
| Mutable variable | let mut x = 0 | set mut x to 0 |
| Reassignment | x = 10 | change x to 10 |
| Function | fn add(a, b) { } | define add(a, b) { } |
| Else branch | else { } | otherwise { } / nah { } |
| Struct definition | struct Point { } | thing Point { } |
| Interface | interface I { } | power I { } |
| Impl block | impl T { } | give T { } |
| Impl for interface | impl I for T { } | give T the power I { } |
| Constructor | Point { x: 1 } | craft Point { x: 1 } |
| Async function | async fn f() { } | forge f() { } |
| Await | await expr | hold expr |
| Yield | yield value | emit value |
| Destructure | let {a, b} = obj | unpack {a, b} from obj |
Innovation Keywords
These keywords introduce constructs unique to Forge that have no direct equivalent in other mainstream languages.
| Keyword | Purpose | Example |
|---|---|---|
when | Guard expression (multi-way conditional) | when age { < 13 -> "kid" } |
unless | Postfix negative conditional | expr unless condition |
until | Postfix loop-until | expr until condition |
must | Crash on error with message | must parse_int(s) |
check | Declarative validation | check name is not empty |
safe | Null-safe execution block | safe { risky_code() } |
where | Collection filter | items where x > 5 |
timeout | Time-limited execution | timeout 5 seconds { } |
retry | Automatic retry with count | retry 3 times { } |
schedule | Cron-style scheduling | schedule every 5 minutes { } |
every | Used with schedule | schedule every N { } |
any | Existential quantifier | any x in items |
ask | AI/LLM prompt | ask "summarize this" |
prompt | Prompt template definition | prompt summarize() { } |
transform | Data transformation block | transform data { } |
table | Table display | table [...] |
select | Query-style select | from X select Y |
order | Query-style ordering | order by field |
by | Used with order and sort | order by name |
limit | Query-style limit | limit 10 |
keep | Filter synonym | keep where condition |
take | Take N items | take 5 |
freeze | Freeze/immobilize a value | freeze expr |
watch | File change detection | watch "file.txt" { } |
download | Download a file from URL | download "url" to "path" |
crawl | Web scraping | crawl "url" |
Error Handling Keywords
| Keyword | Purpose | Example |
|---|---|---|
try | Try block | try { } |
catch | Catch block | catch err { } |
Type Keywords
These identifiers are reserved as built-in type names. They are recognized by the lexer as keyword tokens, not as general identifiers.
| Keyword | Type |
|---|---|
Int | 64-bit signed integer |
Float | 64-bit IEEE 754 float |
String | UTF-8 string |
Bool | Boolean |
Json | JSON value type |
Operators as Keywords
The following operators are lexed as keyword tokens rather than punctuation:
| Token | Keyword | Meaning |
|---|---|---|
|> | Pipe | Pipe-forward operator |
>> | PipeRight | Alternate pipe operator |
... | DotDotDot | Spread operator |
+= | PlusEq | Add-assign |
-= | MinusEq | Subtract-assign |
*= | StarEq | Multiply-assign |
/= | SlashEq | Divide-assign |
Complete Alphabetical Index
For reference, the complete set of keyword strings recognized by the lexer (case-sensitive):
Int, Float, String, Bool, Json,
any, ask, async, await, break, by, catch, change, continue, craft,
crawl, define, download, each, else, emit, every, false, fn, for,
forge, freeze, from, give, grab, hold, if, impl, import, in,
interface, keep, let, limit, loop, match, mut, nah, null, order,
otherwise, power, prompt, pub, repeat, retry, return, safe, say,
schedule, seconds, select, set, spawn, struct, table, take, the,
thing, timeout, times, to, transform, true, try, type, unless,
unpack, until, wait, watch, when, where, while, whisper, yell, yield
All keywords are case-sensitive. Let and LET are identifiers, not keywords. The type keywords Int, Float, String, Bool, and Json are the only keywords that begin with an uppercase letter.
Identifiers
An identifier is a name that refers to a variable, function, type, field, or module. Identifiers are the primary mechanism for binding values to names in Forge programs.
Syntax
Identifier → IdentStart IdentContinue*
IdentStart →
a-z|A-Z|_IdentContinue → IdentStart |
0-9
An identifier begins with an ASCII letter or underscore, followed by zero or more ASCII letters, digits, or underscores. Identifiers have no maximum length.
x
name
_private
camelCase
snake_case
PascalCase
item2
MAX_RETRIES
__internal
Case Sensitivity
Identifiers are case-sensitive. The names name, Name, and NAME refer to three distinct bindings.
let name = "alice"
let Name = "Bob"
let NAME = "CHARLIE"
say name // alice
say Name // Bob
say NAME // CHARLIE
Reserved Words
If an identifier matches a keyword string (see Keywords), it is lexed as that keyword token rather than as an Ident token. Keywords cannot be used as identifiers.
// Error: 'let' is a keyword, not a valid variable name
let let = 5 // parse error
Naming Conventions
Forge does not enforce naming conventions, but the following are idiomatic:
| Element | Convention | Example |
|---|---|---|
| Variables | snake_case | user_name |
| Functions | snake_case | get_user |
| Types (structs) | PascalCase | HttpRequest |
| Interfaces | PascalCase | Describable |
| Constants | UPPER_SNAKE | MAX_RETRIES |
| Modules | snake_case | math, fs, json |
The it Identifier
The identifier it has special meaning inside method blocks defined with give (or impl). When used as the first parameter of a method, it refers to the receiver instance — the object on which the method was called.
thing Person {
name: String,
age: Int
}
give Person {
define greet(it) {
return "Hi, I'm " + it.name
}
}
set p to craft Person { name: "Alice", age: 30 }
say p.greet() // Hi, I'm Alice
When p.greet() is called, the value of p is automatically bound to it inside the method body. The caller does not pass it explicitly.
If the first parameter of a method is not named it, the method is treated as a static method — it is called on the type itself rather than on an instance:
give Person {
define infant(name) {
return craft Person { name: name, age: 0 }
}
}
set baby to Person.infant("Bob")
Outside of method blocks, it has no special meaning and may be used as an ordinary identifier, though this is discouraged for clarity.
Underscore
A lone underscore (_) is a valid identifier. By convention, it is used as a placeholder for values that are intentionally ignored:
match result {
Ok(_) => say "success"
Err(msg) => say "error: {msg}"
}
for _, value in enumerate(items) {
say value
}
Shadowing
A new let or set declaration may reuse an identifier that is already in scope. The new binding shadows the previous one within the inner scope:
let x = 10
say x // 10
if true {
let x = 20
say x // 20 (shadows outer x)
}
say x // 10 (outer x is unchanged)
Shadowing creates a new binding; it does not mutate the original variable.
Literals
A literal is a notation for representing a fixed value in source code. Forge supports integer, float, string, raw string, boolean, null, array, and object literals.
Integer Literals
IntLiteral → Digit+
Digit →
0-9
An integer literal is a sequence of one or more decimal digits. Integer literals produce values of type Int (64-bit signed integer).
0
42
1000000
Negative integer values are expressed using the unary negation operator:
let x = -42
Integer literals do not support underscores as digit separators, hexadecimal, octal, or binary notation in the current version.
Float Literals
FloatLiteral → Digit+
.Digit+
A float literal contains a decimal point with digits on both sides. Float literals produce values of type Float (64-bit IEEE 754 double-precision).
3.14
0.5
100.0
A leading dot (.5) or trailing dot (5.) is not valid. Both sides of the decimal point must have at least one digit.
Negative float values use unary negation:
let temp = -0.5
Scientific notation (e.g., 1.5e10) is not supported in the current version.
String Literals
StringLiteral →
"StringContent*"StringContent → Character | EscapeSequence | Interpolation
EscapeSequence →
\n|\t|\\|\"Interpolation →
{Expression}
A string literal is a sequence of characters enclosed in double quotes. String literals produce values of type String (UTF-8 encoded, immutable).
"hello, world"
"line one\nline two"
"she said \"hi\""
Escape Sequences
The following escape sequences are recognized within string literals:
| Sequence | Character |
|---|---|
\n | Newline (U+000A) |
\t | Horizontal tab (U+0009) |
\\ | Backslash (U+005C) |
\" | Double quote (U+0022) |
String Interpolation
Curly braces within a string literal delimit an interpolation expression. The expression is evaluated at runtime, converted to a string, and inserted at that position:
let name = "Forge"
let version = 3
say "Welcome to {name} v{version}!"
// Output: Welcome to Forge v3!
Interpolation supports arbitrary expressions, not just variable names:
let x = 7
say "Seven squared is {x * x}"
say "Length: {len("hello")}"
say "Upper: {name}"
To include a literal { in a string without triggering interpolation, there is no dedicated escape sequence in the current version. Use string concatenation or a variable if needed.
Raw String Literals
RawStringLiteral →
"""RawContent*"""
A raw string literal is delimited by triple double quotes. Raw strings preserve their content exactly as written: no escape sequences are processed and no interpolation occurs.
let sql = """SELECT * FROM users WHERE active = true"""
let html = """<div class="container">
<h1>Hello</h1>
</div>"""
Raw strings may span multiple lines. They are particularly useful for SQL queries, regular expressions, and embedded markup.
Boolean Literals
BoolLiteral →
true|false
The boolean literals true and false produce values of type Bool. They are keyword tokens.
let active = true
let deleted = false
Null Literal
NullLiteral →
null
The null literal represents the absence of a value. It produces a value of type Null. It is a keyword token.
let nothing = null
say typeof(nothing) // Null
Array Literals
ArrayLiteral →
[( Expression (,Expression )*,? )?]
An array literal is a comma-separated list of expressions enclosed in square brackets. Arrays are ordered, heterogeneous (elements may have different types), and 0-indexed.
let empty = []
let nums = [1, 2, 3]
let mixed = [1, "two", true, null]
let nested = [[1, 2], [3, 4]]
A trailing comma after the last element is permitted.
Object Literals
ObjectLiteral →
{( Field (,Field )*,? )?}Field → Identifier
:Expression
An object literal is a comma-separated list of key-value pairs enclosed in curly braces. Keys are identifiers (unquoted). Objects maintain insertion order.
let empty = {}
let user = { name: "Alice", age: 30 }
let config = {
host: "localhost",
port: 8080,
debug: false,
}
Object keys are strings at runtime, even though they appear as bare identifiers in the literal syntax. A trailing comma after the last field is permitted.
Shorthand Field Syntax
When a variable name matches the desired key name, the value may be omitted:
let name = "Alice"
let age = 30
let user = { name, age }
// Equivalent to: { name: "Alice", age: 30 }
Operators and Punctuation
This section defines all operator and punctuation tokens in Forge.
Arithmetic Operators
| Token | Name | Example | Description |
|---|---|---|---|
+ | Plus | a + b | Addition; string concatenation |
- | Minus | a - b | Subtraction; unary negation |
* | Star | a * b | Multiplication |
/ | Slash | a / b | Division |
% | Percent | a % b | Modulo (remainder) |
When both operands of / are integers, the result is an integer (truncating division). When either operand is a float, the result is a float.
The + operator is overloaded for string concatenation when both operands are strings.
Comparison Operators
| Token | Name | Example | Description |
|---|---|---|---|
== | Equal | a == b | Equality test |
!= | Not equal | a != b | Inequality test |
< | Less than | a < b | Less-than comparison |
> | Greater than | a > b | Greater-than comparison |
<= | Less than or equal | a <= b | Less-than-or-equal |
>= | Greater than or equal | a >= b | Greater-than-or-equal |
All comparison operators return a Bool value. Strings are compared lexicographically.
Logical Operators
| Token | Name | Example | Description |
|---|---|---|---|
&& | Logical AND | a && b | Short-circuit conjunction |
|| | Logical OR | a || b | Short-circuit disjunction |
! | Logical NOT | !a | Unary boolean negation |
The keywords and and or are not reserved keywords in Forge. Logical operations use the symbolic && and || forms exclusively. The not keyword is also not reserved; use the ! prefix operator.
Both && and || use short-circuit evaluation: the right operand is not evaluated if the result can be determined from the left operand alone.
Assignment Operators
| Token | Name | Example | Equivalent |
|---|---|---|---|
= | Assignment | x = 5 | — |
+= | Add-assign | x += 5 | x = x + 5 |
-= | Subtract-assign | x -= 3 | x = x - 3 |
*= | Multiply-assign | x *= 2 | x = x * 2 |
/= | Divide-assign | x /= 4 | x = x / 4 |
Assignment and compound assignment operators require the left-hand side to be a mutable variable (declared with mut). Compound assignment is syntactic sugar for the expanded form.
Member Access and Navigation
| Token | Name | Example | Description |
|---|---|---|---|
. | Dot | obj.field | Field access, method call |
.. | Range | 1..10 | Range constructor (exclusive end) |
The dot operator accesses fields on objects and struct instances, and invokes methods. It binds very tightly (highest precedence among binary operators).
The range operator .. creates a range value from a start (inclusive) to an end (exclusive). It is used primarily with for loops and the range() built-in.
Pipe Operators
| Token | Name | Example | Description |
|---|---|---|---|
|> | Pipe | x |> f | Pipe-forward: passes left as first argument to right |
>> | Pipe right | x >> f | Alternate pipe operator |
The pipe operator passes the value on the left as the first argument to the function on the right:
let result = [3, 1, 4, 1, 5]
|> sort
|> reverse
Spread Operator
| Token | Name | Example | Description |
|---|---|---|---|
... | Spread | [...arr, 4] | Spreads array/object elements |
The spread operator expands an array or object into individual elements within an array literal or object literal:
let a = [1, 2, 3]
let b = [...a, 4, 5] // [1, 2, 3, 4, 5]
let base = { x: 1, y: 2 }
let ext = { ...base, z: 3 } // { x: 1, y: 2, z: 3 }
Arrow Operators
| Token | Name | Example | Description |
|---|---|---|---|
-> | Arrow | < 13 -> "kid" | Arm separator in when/match |
=> | Fat arrow | Ok(v) => say v | Arm separator in match |
The thin arrow -> is used in when guard arms. The fat arrow => is used in match pattern arms. Both separate a pattern/condition from its corresponding body.
Special Operators
| Token | Name | Example | Description |
|---|---|---|---|
? | Question | expr? | Error propagation (Result/Option) |
@ | At | @test | Decorator prefix |
& | Ampersand | (reserved) | Reserved for future use |
| | Bar | Circle(r) | Rect(w, h) | ADT variant separator |
The ? postfix operator propagates errors: if the expression evaluates to Err(e), the enclosing function returns Err(e) immediately. If the expression is Ok(v), the ? unwraps it to v.
Delimiters
| Token | Name | Purpose |
|---|---|---|
( | Left paren | Function call, grouping |
) | Right paren | Close function call, grouping |
{ | Left brace | Block, object literal, interpolation |
} | Right brace | Close block, object, interpolation |
[ | Left bracket | Array literal, index access |
] | Right bracket | Close array, index access |
, | Comma | Separator in lists |
: | Colon | Key-value separator, type annotation |
; | Semicolon | Optional statement terminator |
Operator Precedence
Operators are listed from highest to lowest precedence:
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (highest) | . (member access) | Left-to-right |
| 2 | () (call), [] (index) | Left-to-right |
| 3 | !, - (unary) | Right-to-left |
| 4 | *, /, % | Left-to-right |
| 5 | +, - | Left-to-right |
| 6 | .. | Left-to-right |
| 7 | <, >, <=, >= | Left-to-right |
| 8 | ==, != | Left-to-right |
| 9 | && | Left-to-right |
| 10 | || | Left-to-right |
| 11 | |> | Left-to-right |
| 12 | ? | Postfix |
| 13 (lowest) | =, +=, -=, *=, /= | Right-to-left |
Parentheses may be used to override the default precedence.
Comments
Comments are annotations in source code intended for human readers. The lexer recognizes comments and discards them; they do not appear in the token stream and have no effect on program behavior.
Forge supports two forms of comments: line comments and block comments.
Line Comments
LineComment →
//Character* Newline
A line comment begins with // and extends to the end of the line (the next newline character or end of file). Everything after // on that line is ignored by the lexer.
// This is a line comment
let x = 42 // This is an inline comment
Line comments may appear on their own line or at the end of a line containing code.
Block Comments
BlockComment →
/*Character**/
A block comment begins with /* and ends with the next occurrence of */. Block comments may span multiple lines.
/* This is a block comment */
/*
This block comment
spans multiple lines.
*/
let x = /* inline block comment */ 42
Block comments do not nest. The first */ encountered after a /* ends the comment, regardless of any intervening /* sequences:
/* outer /* inner */ this is NOT inside the comment */
In the example above, the comment ends at the first */, and the text this is NOT inside the comment */ would be parsed as code (and likely produce a syntax error).
Doc Comments
Forge does not currently have a dedicated doc comment syntax (such as /// or /** */). Documentation is written using regular line comments or block comments by convention.
Comments in String Literals
Comment sequences (// and /* */) within string literals are part of the string content and are not treated as comments:
let url = "https://example.com" // The // in the string is not a comment
let msg = "use /* carefully */" // The /* */ in the string is not a comment
Placement
Comments may appear anywhere that whitespace is permitted. They cannot appear inside tokens (e.g., in the middle of an identifier or numeric literal).
Types
This chapter describes the type system of the Forge programming language.
Overview
Forge is dynamically typed at runtime: variables do not have fixed types, and any variable may hold a value of any type at any point during execution. However, Forge supports optional type annotations (gradual typing) on variable declarations, function parameters, and return types. These annotations serve as documentation and enable the optional type checker to detect certain errors before execution.
Every value in Forge belongs to exactly one of the following type categories:
| Category | Types |
|---|---|
| Primitive | Int, Float, String, Bool, Null |
| Collection | Array, Object |
| Struct | User-defined via struct / thing |
| Interface | User-defined via interface / power |
| Function | Named functions, closures, lambdas |
| Algebraic (ADT) | User-defined via type Name = Variant | ... |
| Result | Ok(value), Err(message) |
| Option | Some(value), None |
Type Annotations
Type annotations use a colon after the name, followed by the type:
let name: String = "Alice"
let age: Int = 30
let score: Float = 98.5
let active: Bool = true
Function parameters and return types may also be annotated:
fn add(a: Int, b: Int) -> Int {
return a + b
}
When annotations are omitted, types are inferred from the assigned values. Annotations are always optional.
Type Inspection at Runtime
The built-in typeof() function (aliased as type()) returns a string describing the runtime type of a value:
say typeof(42) // Int
say typeof(3.14) // Float
say typeof("hello") // String
say typeof(true) // Bool
say typeof(null) // Null
say typeof([1, 2, 3]) // Array
say typeof({ name: "Alice" }) // Object
For struct instances, typeof() returns the struct name (e.g., "Person").
Truthiness
When a value is used in a boolean context (such as an if condition), Forge evaluates it as “truthy” or “falsy” according to the following rules:
| Value | Truthy? |
|---|---|
false | Falsy |
null | Falsy |
0 (integer zero) | Falsy |
0.0 (float zero) | Falsy |
"" (empty string) | Falsy |
[] (empty array) | Falsy |
| Everything else | Truthy |
Subsections
The following subsections define each type category in detail:
- Primitive Types — Int, Float, String, Bool, Null
- Collection Types — Array, Object
- Struct Types —
struct/thingdefinitions - Interface Types —
interface/powercontracts - Function Types — functions as first-class values
- Algebraic Data Types —
type Name = Variant | ... - Option and Result —
OptionandResultwrapper types - Type Conversions —
str(),int(),float(),type(),typeof()
Primitive Types
Forge has five primitive types. Primitive values are immutable and compared by value.
Int
The Int type represents a 64-bit signed integer. Its range is -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 (i.e., the range of a Rust i64).
let x = 42
let y = -7
let z = 0
say typeof(x) // Int
Integer arithmetic follows standard rules. Division between two integers produces an integer result (truncating toward zero):
say 7 / 2 // 3
say -7 / 2 // -3
Integer overflow behavior is implementation-defined. The interpreter wraps on overflow (Rust’s default for i64 in release mode).
Float
The Float type represents a 64-bit IEEE 754 double-precision floating-point number. This provides approximately 15-17 significant decimal digits of precision.
let pi = 3.14159
let temp = -0.5
let one = 1.0
say typeof(pi) // Float
When an arithmetic operation involves both an Int and a Float, the integer is implicitly promoted to a float, and the result is a Float:
say 5 + 2.0 // 7.0
say 10 / 3.0 // 3.3333333333333335
Special float values (NaN, Infinity, -Infinity) may arise from operations like division by zero on floats, but there is no literal syntax for these values.
String
The String type represents an immutable sequence of UTF-8 encoded characters. Strings are created using double-quoted literals and support interpolation and escape sequences (see Literals).
let greeting = "Hello, World!"
let empty = ""
let multiline = """This is
a raw string."""
say typeof(greeting) // String
Key Properties
- Immutable. String operations always return new strings; the original is never modified.
- UTF-8. All strings are valid UTF-8. The
len()function returns the number of bytes, not Unicode code points. - Interpolation. Double-quoted strings support
{expr}interpolation. Raw strings ("""...""") do not. - Concatenation. The
+operator concatenates two strings.
let name = "Forge"
let version = 3
say "Welcome to {name} v{version}!" // Welcome to Forge v3!
say "Hello" + ", " + "World!" // Hello, World!
String Comparison
Strings are compared lexicographically (byte-by-byte) using the standard comparison operators:
say "apple" < "banana" // true
say "hello" == "hello" // true
Bool
The Bool type has exactly two values: true and false.
let active = true
let deleted = false
say typeof(active) // Bool
Boolean values result from comparison operators (==, !=, <, >, <=, >=) and logical operators (&&, ||, !). They are the natural type for if conditions, while conditions, and other control flow predicates.
Logical Operations
| Expression | Result |
|---|---|
true && true | true |
true && false | false |
false || true | true |
!true | false |
Both && and || use short-circuit evaluation.
Null
The Null type has exactly one value: null. It represents the absence of a meaningful value.
let nothing = null
say typeof(nothing) // Null
null is returned by functions that have no explicit return statement. It is falsy in boolean contexts.
Null vs. None
Forge distinguishes between null and None:
nullis a bare value representing “no value.” It is a primitive.Noneis a variant of theOptiontype representing “intentionally absent.” It is a wrapper.
In practice, null appears in dynamic code and untyped contexts, while None is used in the Option/Result error-handling pattern. See Option and Result for details.
Type Comparison Summary
| Type | Size | Default Value | Falsy Values | Mutable |
|---|---|---|---|---|
Int | 64 bits | — | 0 | No |
Float | 64 bits | — | 0.0 | No |
String | Variable | — | "" | No |
Bool | 1 bit | — | false | No |
Null | 0 bits | null | null | No |
All primitive types are compared by value. Two integers with the same numeric value are equal regardless of how they were computed.
Collection Types
Forge has two built-in collection types: Array and Object. Both are mutable, heap-allocated, and compared by reference for identity but by value for equality.
Array
An array is an ordered, heterogeneous, 0-indexed sequence of values. Elements may be of any type, including other arrays and objects.
Creation
Arrays are created with square bracket literals:
let empty = []
let nums = [1, 2, 3]
let mixed = [1, "two", true, null]
let nested = [[1, 2], [3, 4]]
Indexing
Elements are accessed by zero-based integer index using bracket notation:
let fruits = ["apple", "banana", "cherry"]
say fruits[0] // apple
say fruits[1] // banana
say fruits[2] // cherry
Negative indices count from the end of the array:
say fruits[-1] // cherry
Accessing an out-of-bounds index produces a runtime error.
Mutation
Arrays are mutable. Elements can be replaced by index assignment:
let mut items = [1, 2, 3]
items[0] = 10
say items // [10, 2, 3]
The push() built-in appends an element:
let mut items = [1, 2]
push(items, 3)
say items // [1, 2, 3]
The pop() built-in removes and returns the last element:
let mut items = [1, 2, 3]
let last = pop(items)
say last // 3
say items // [1, 2]
Length
The len() built-in returns the number of elements:
say len([1, 2, 3]) // 3
say len([]) // 0
Spread
The spread operator ... expands an array within another array literal:
let a = [1, 2, 3]
let b = [...a, 4, 5]
say b // [1, 2, 3, 4, 5]
Iteration
Arrays are iterable with for/in:
for item in [10, 20, 30] {
say item
}
With enumerate() for index-value pairs:
for i, item in enumerate(["a", "b", "c"]) {
say "{i}: {item}"
}
Functional Operations
Arrays support map, filter, reduce, sort, reverse, find, flat_map, any, all, and other functional built-ins:
let nums = [1, 2, 3, 4, 5]
let doubled = map(nums, fn(x) { return x * 2 })
let evens = filter(nums, fn(x) { return x % 2 == 0 })
let sum = reduce(nums, 0, fn(acc, x) { return acc + x })
Truthiness
An empty array [] is falsy. All non-empty arrays are truthy.
Object
An object is an insertion-ordered map from string keys to arbitrary values. Objects are Forge’s general-purpose key-value data structure.
Creation
Objects are created with curly brace literals:
let empty = {}
let user = { name: "Alice", age: 30 }
let config = {
host: "localhost",
port: 8080,
debug: false,
}
Keys are written as bare identifiers in the literal syntax. At runtime, they are strings.
Field Access
Fields are accessed with dot notation or bracket notation:
let user = { name: "Alice", age: 30 }
say user.name // Alice
say user["age"] // 30
Dot notation requires the field name to be a valid identifier. Bracket notation accepts any string expression, making it suitable for dynamic keys:
let key = "name"
say user[key] // Alice
Mutation
Objects are mutable. Fields can be added, updated, or accessed dynamically:
let mut obj = { x: 1 }
obj.y = 2
obj.x = 10
say obj // { x: 10, y: 2 }
Key Operations
| Function | Description |
|---|---|
keys(obj) | Returns array of keys |
values(obj) | Returns array of values |
entries(obj) | Returns array of [key, value] pairs |
has_key(obj, k) | Returns true if key exists |
len(obj) | Returns number of key-value pairs |
merge(a, b) | Returns new object merging b into a |
pick(obj, ks) | Returns object with only specified keys |
omit(obj, ks) | Returns object without specified keys |
Spread
The spread operator expands an object within another object literal:
let base = { x: 1, y: 2 }
let ext = { ...base, z: 3 }
say ext // { x: 1, y: 2, z: 3 }
When keys conflict, later values overwrite earlier ones:
let a = { x: 1, y: 2 }
let b = { ...a, x: 10 }
say b // { x: 10, y: 2 }
Iteration
Objects are iterable. A for/in loop over an object yields [key, value] pairs in insertion order:
let user = { name: "Alice", age: 30 }
for key, value in user {
say "{key}: {value}"
}
Truthiness
An empty object {} is truthy (unlike empty arrays, which are falsy). All objects, including empty ones, are truthy.
Objects vs. Structs
Plain objects are untyped: any object can have any set of keys. Structs (defined with struct or thing) provide named types with declared fields, type annotations, default values, and methods. Under the hood, struct instances are objects with a __type__ field. See Struct Types for details.
Struct Types
A struct defines a named data type with declared fields, optional type annotations, and optional default values. Structs are the primary mechanism for defining domain-specific types in Forge.
Definition
Structs are defined with the struct keyword (classic syntax) or the thing keyword (natural syntax). Both forms are equivalent.
StructDef → (
struct|thing) Identifier{FieldList}FieldList → ( Field (
,Field )*,? )?Field → Identifier
:TypeAnnotation (=Expression )? |hasIdentifier:TypeAnnotation
// Classic syntax
struct Point {
x: Int,
y: Int
}
// Natural syntax — identical result
thing Point {
x: Int,
y: Int
}
Field type annotations are part of the struct definition syntax. Each field has a name, a colon, and a type name.
Default Values
Fields may have default values. If a field with a default is omitted during construction, the default value is used:
thing Config {
host: String = "localhost",
port: Int = 8080,
debug: Bool = false
}
set cfg to craft Config {}
say cfg.host // localhost
say cfg.port // 8080
say cfg.debug // false
Fields without defaults are required — omitting them during construction produces a runtime error.
set prod to craft Config { host: "api.example.com", port: 443 }
say prod.host // api.example.com
say prod.debug // false (default)
Construction
There are two ways to create a struct instance:
Direct Construction (Classic)
Use the type name followed by a field initializer block:
let p = Point { x: 3, y: 4 }
Craft Construction (Natural)
Use the craft keyword:
set p to craft Point { x: 3, y: 4 }
Both forms produce the same value: an object with the declared fields plus a __type__ field set to the struct name.
Field Access
Fields are accessed with dot notation:
let p = Point { x: 3, y: 4 }
say p.x // 3
say p.y // 4
The __type__ Field
Every struct instance has an internal __type__ field containing the struct name as a string. This field is set automatically during construction and is used by the runtime for method dispatch, interface satisfaction checking, and typeof():
let p = Point { x: 3, y: 4 }
say typeof(p) // Point
Methods
Methods are attached to a struct using give (natural) or impl (classic) blocks. See Method Blocks for full details.
give Point {
define distance(it) {
return math.sqrt(it.x * it.x + it.y * it.y)
}
}
let p = Point { x: 3, y: 4 }
say p.distance() // 5.0
The first parameter it is the receiver. When p.distance() is called, p is automatically bound to it.
Static Methods
If a method’s first parameter is not it, it is a static method called on the type rather than on an instance:
give Person {
define infant(name) {
return craft Person { name: name, age: 0 }
}
}
set baby to Person.infant("Bob")
Multiple give Blocks
Multiple give blocks for the same type are permitted. Methods accumulate across all blocks:
give Person {
define greet(it) {
return "Hi, I'm " + it.name
}
}
give Person {
define birthday(it) {
return craft Person { name: it.name, age: it.age + 1 }
}
}
Composition with has
The has keyword inside a struct body embeds one type within another, enabling field and method delegation:
thing Address {
street: String,
city: String
}
thing Employee {
name: String,
has addr: Address
}
The has keyword provides two delegation mechanisms:
-
Field delegation. Accessing a field that does not exist on the outer type delegates to the embedded type.
emp.cityresolves toemp.addr.city. -
Method delegation. Calling a method that does not exist on the outer type delegates to the embedded type.
emp.full()resolves toemp.addr.full().
give Address {
define full(it) {
return it.street + ", " + it.city
}
}
set emp to craft Employee {
name: "Charlie",
addr: craft Address { street: "123 Main St", city: "Portland" }
}
say emp.city // Portland (delegated)
say emp.full() // 123 Main St, Portland (delegated)
The explicit path (emp.addr.city) also works and produces the same result.
Struct vs. Object
| Feature | Object | Struct |
|---|---|---|
| Type identity | None (generic Object) | Named (__type__ field) |
| Field declarations | No | Yes, with type annotations |
| Default values | No | Yes |
| Methods | No | Yes (via give/impl) |
| Interface satisfaction | No | Yes (via give...the power) |
| Construction | { key: value } | Name { } or craft Name { } |
Interface Types
An interface (or “power” in natural syntax) defines a contract: a set of method signatures that a type must provide. Interfaces enable polymorphism in Forge — any type that satisfies an interface’s contract can be used wherever that interface is expected.
Definition
Interfaces are defined with the interface keyword (classic) or the power keyword (natural).
InterfaceDef → (
interface|power) Identifier{MethodSignature*}MethodSignature →
fnIdentifier(ParamList?)(->Type )?
// Classic syntax
interface Describable {
fn describe() -> String
}
// Natural syntax — identical result
power Describable {
fn describe() -> String
}
An interface body contains one or more method signatures. Each signature specifies the method name, parameter types, and optional return type. No method body is provided — interfaces declare what methods must exist, not how they work.
Implementing an Interface
A type satisfies an interface by providing implementations of all required methods. This is done using give ... the power ... (natural) or impl ... for ... (classic).
thing Person {
name: String,
age: Int
}
// Natural syntax
give Person the power Describable {
define describe(it) {
return it.name + " (" + str(it.age) + ")"
}
}
// Classic syntax — identical result
impl Describable for Person {
fn describe(it) {
return it.name + " (" + str(it.age) + ")"
}
}
The implementation block must provide a method for every signature in the interface. Missing methods produce a compile-time (definition-time) error.
Checking Satisfaction
The satisfies() built-in function checks whether a value’s type satisfies a given interface at runtime:
set alice to craft Person { name: "Alice", age: 30 }
say satisfies(alice, Describable) // true
If a type has not implemented the interface, satisfies() returns false:
thing Robot {
id: Int
}
set r to craft Robot { id: 1 }
say satisfies(r, Describable) // false
Multiple Interfaces
A single type may implement multiple interfaces:
power Describable {
fn describe() -> String
}
power Vocal {
fn speak() -> String
}
thing Animal {
species: String,
sound: String
}
give Animal the power Describable {
define describe(it) {
return it.species + " that says " + it.sound
}
}
give Animal the power Vocal {
define speak(it) {
return it.sound + "! " + it.sound + "!"
}
}
Each interface implementation is provided in a separate give...the power block. A type accumulates all its interface implementations.
No Default Implementations
In the current version of Forge, interfaces cannot provide default method implementations. Every method in an interface must be explicitly implemented by each type that satisfies it.
Interface Inheritance
Interface inheritance (one interface extending another) is not supported in the current version. Each interface is independent.
Structural vs. Nominal
Forge uses a nominal approach to interface satisfaction: a type satisfies an interface only if it has been explicitly declared to do so via a give...the power or impl...for block. Simply having methods with matching names and signatures is not sufficient.
However, the satisfies() built-in performs a structural check at runtime — it verifies that the required methods actually exist on the value. This means satisfaction is ultimately determined by the presence of the declared methods, but the declaration is required to register the type-interface relationship.
Function Types
Functions are first-class values in Forge. They can be stored in variables, passed as arguments, returned from other functions, and placed in data structures.
Named Functions
A named function is declared with fn (classic) or define (natural):
fn add(a, b) {
return a + b
}
define multiply(a, b) {
return a * b
}
Named functions are hoisted within their scope — they can be called before their textual definition in the source file.
Anonymous Functions (Closures)
An anonymous function is created with the fn keyword in expression position:
let double = fn(x) { return x * 2 }
say double(21) // 42
Anonymous functions are also called closures because they capture variables from their enclosing scope.
Closure Semantics
Closures capture variables from the enclosing scope by reference. The closure retains access to the captured variables for its entire lifetime:
fn make_counter() {
let mut count = 0
return fn() {
count = count + 1
return count
}
}
let counter = make_counter()
say counter() // 1
say counter() // 2
say counter() // 3
Each call to make_counter() creates a new independent closure with its own count variable.
Factory Pattern
Closures are commonly used to create specialized functions:
fn make_adder(n) {
return fn(x) {
return x + n
}
}
let add5 = make_adder(5)
let add10 = make_adder(10)
say add5(3) // 8
say add10(3) // 13
Functions as Arguments (Higher-Order Functions)
Functions can be passed as arguments to other functions:
fn apply(f, value) {
return f(value)
}
fn square(x) { return x * x }
say apply(square, 7) // 49
The built-in map, filter, reduce, and sort functions all accept function arguments:
let nums = [1, 2, 3, 4, 5]
let doubled = map(nums, fn(x) { return x * 2 })
say doubled // [2, 4, 6, 8, 10]
Functions in Data Structures
Functions can be stored in arrays and objects:
fn add(a, b) { return a + b }
fn sub(a, b) { return a - b }
let ops = [add, sub]
say ops[0](10, 3) // 13
say ops[1](10, 3) // 7
let handlers = {
greet: fn(name) { return "Hello, {name}!" },
farewell: fn(name) { return "Goodbye, {name}!" }
}
say handlers.greet("World") // Hello, World!
Type-Annotated Functions
Function parameters and return values may carry type annotations:
fn add(a: Int, b: Int) -> Int {
return a + b
}
fn format_price(amount: Float) -> String {
return "${amount}"
}
Annotations are optional and serve as documentation. The optional type checker can use them to report errors before execution.
Return Values
Functions return a value via the return statement. If no return is executed, the function returns null:
fn greet(name) {
println("Hello, {name}!")
}
let result = greet("World")
say typeof(result) // Null
A function may return early from any point:
fn classify(n) {
if n < 0 { return "negative" }
if n == 0 { return "zero" }
return "positive"
}
Async Functions
Async functions are declared with async fn (classic) or forge (natural):
// Classic
async fn fetch_data() {
let resp = await http.get("https://api.example.com/data")
return resp
}
// Natural
forge fetch_data() {
let resp = hold http.get("https://api.example.com/data")
return resp
}
Async functions return a future that must be awaited with await (classic) or hold (natural). See Concurrency for details.
Function Equality
Functions are compared by reference identity, not by their code. Two function values are equal only if they refer to the same function object:
fn f() { return 1 }
let a = f
let b = f
say a == b // true (same function object)
let c = fn() { return 1 }
say a == c // false (different function objects)
Algebraic Data Types
Algebraic data types (ADTs) define a type as a fixed set of variants. Each variant may optionally carry data. ADTs enable exhaustive pattern matching — the compiler/runtime can verify that all variants are handled.
Definition
An ADT is defined with the type keyword, listing variants separated by |:
ADTDef →
typeIdentifier=Variant (|Variant )*Variant → Identifier (
(TypeList))?TypeList → Type (
,Type )*
Unit Variants
Variants without data fields are called unit variants:
type Color = Red | Green | Blue
Unit variants are used as simple enumeration values:
set c to Red
say c // Red
Data Variants
Variants may carry typed data fields:
type Shape = Circle(Float) | Rect(Float, Float)
Data variants are constructed by calling the variant name as a function:
set circle to Circle(5.0)
set rect to Rect(3.0, 4.0)
say circle // Circle(5.0)
say rect // Rect(3.0, 4.0)
Mixed Variants
An ADT may freely mix unit variants and data variants:
type Result = Ok(String) | Err(String) | Pending
Construction
Unit variants are referenced by name alone:
let color = Red
Data variants are constructed by calling the variant name with the appropriate arguments:
let shape = Circle(5.0)
let rect = Rect(3.0, 4.0)
The number and types of arguments must match the variant definition.
Pattern Matching
ADT values are destructured using match expressions. Each arm matches a variant and optionally binds its data fields to variables.
Basic Matching
type Color = Red | Green | Blue
let c = Red
match c {
Red => say "Red!"
Green => say "Green!"
Blue => say "Blue!"
}
Destructuring Data Variants
Data fields are bound to named variables in the match arm:
type Shape = Circle(Float) | Rect(Float, Float)
define describe_shape(s) {
match s {
Circle(r) => {
say "Circle with radius {r}, area = {3.14159 * r * r}"
}
Rect(w, h) => {
say "Rectangle {w}x{h}, area = {w * h}"
}
}
}
describe_shape(Circle(5.0))
// Output: Circle with radius 5.0, area = 78.53975
describe_shape(Rect(3.0, 4.0))
// Output: Rectangle 3.0x4.0, area = 12.0
Match as Expression
match can be used as an expression that returns a value:
let area = match shape {
Circle(r) => 3.14159 * r * r
Rect(w, h) => w * h
}
Exhaustiveness
A match expression on an ADT should handle all variants. If a variant is missing, the runtime will produce an error when an unhandled variant is encountered.
type Color = Red | Green | Blue
// This handles all variants
match c {
Red => say "Red!"
Green => say "Green!"
Blue => say "Blue!"
}
Wildcard Pattern
The _ pattern matches any value, serving as a catch-all:
match c {
Red => say "It's red"
_ => say "It's not red"
}
Built-in ADTs
Forge provides two built-in algebraic types:
- Option:
Some(value) | None— see Option and Result - Result:
Ok(value) | Err(message)— see Option and Result
These follow the same pattern matching conventions as user-defined ADTs:
let x = Some(42)
match x {
Some(val) => say "Got: {val}"
None => say "Nothing"
}
Scope
Variant constructors (e.g., Red, Circle, Some, Ok) are introduced into the scope where the type definition appears. For built-in types like Option and Result, the constructors are globally available.
Option and Result
Forge provides two built-in wrapper types for representing optional values and fallible operations: Option and Result. These types enable explicit, composable error handling without exceptions.
Option Type
The Option type represents a value that may or may not be present.
Variants
| Variant | Meaning |
|---|---|
Some(value) | A value is present |
None | No value is present |
Construction
let x = Some(42)
let y = None
Both Some and None are globally available constructors.
Inspection
| Function | Description | Example |
|---|---|---|
is_some(v) | Returns true if v is Some(...) | is_some(Some(1)) = true |
is_none(v) | Returns true if v is None | is_none(None) = true |
let x = Some(42)
say is_some(x) // true
say is_none(x) // false
let y = None
say is_some(y) // false
say is_none(y) // true
Pattern Matching
Option values are destructured with match:
let value = Some(42)
match value {
Some(v) => say "Got: {v}"
None => say "Nothing"
}
Unwrapping
| Function | Description |
|---|---|
unwrap(v) | Returns the inner value; crashes if None |
unwrap_or(v, d) | Returns the inner value, or d if None |
let x = Some(42)
say unwrap(x) // 42
say unwrap_or(x, 0) // 42
let y = None
say unwrap_or(y, 0) // 0
// unwrap(y) would crash with an error
Result Type
The Result type represents the outcome of an operation that may succeed or fail.
Variants
| Variant | Meaning |
|---|---|
Ok(value) | Operation succeeded with value |
Err(message) | Operation failed with message |
Construction
let success = Ok(42)
let failure = Err("something went wrong")
Result constructors accept both cases: Ok(42) and ok(42) are equivalent, as are Err("msg") and err("msg").
Inspection
| Function | Description | Example |
|---|---|---|
is_ok(v) | Returns true if v is Ok(...) | is_ok(Ok(1)) = true |
is_err(v) | Returns true if v is Err(...) | is_err(Err("x")) = true |
let result = Ok(42)
say is_ok(result) // true
say is_err(result) // false
Pattern Matching
Result values are destructured with match:
fn parse_number(s) {
let n = int(s)
if n == null {
return Err("invalid number: {s}")
}
return Ok(n)
}
match parse_number("42") {
Ok(n) => say "Parsed: {n}"
Err(msg) => say "Error: {msg}"
}
Unwrapping
| Function | Description |
|---|---|
unwrap(v) | Returns the inner value; crashes if Err |
unwrap_or(v, d) | Returns the inner value, or d if Err |
let result = Ok(42)
say unwrap(result) // 42
say unwrap_or(result, 0) // 42
let err = Err("failed")
say unwrap_or(err, 0) // 0
The ? Operator
The ? postfix operator provides concise error propagation. When applied to a Result value:
- If the value is
Ok(v), the?unwraps it tovand execution continues. - If the value is
Err(e), the enclosing function immediately returnsErr(e).
fn read_config(path) {
if !fs.exists(path) {
return Err("config file not found")
}
return Ok(fs.read(path))
}
fn start_server() {
let config = read_config("server.toml")?
say "Starting with config: {config}"
return Ok(true)
}
match start_server() {
Ok(_) => say "Server started"
Err(msg) => say "Failed: {msg}"
}
The ? operator can only be used inside functions that return Result. It is syntactic sugar for:
let result = read_config("server.toml")
if is_err(result) {
return result
}
let config = unwrap(result)
The must Keyword
The must keyword is an assertion on Result values. It unwraps an Ok value or crashes the program with a clear error message on Err:
let config = must read_config("server.toml")
Use must for errors that are truly unrecoverable — situations where the program cannot meaningfully continue (e.g., missing configuration, failed database connection).
The safe Block
The safe block catches any errors within its body and returns null instead of crashing:
safe {
let result = risky_operation()
say result
}
// If risky_operation() fails, execution continues here
safe is a statement-level construct. It does not return a value and cannot be used as an expression.
Idiomatic Error Handling
The recommended patterns for error handling in Forge, in order of preference:
?for propagation. Pass errors up the call stack to a centralized handler.matchfor handling. Explicitly handle bothOkandErrat the appropriate level.unwrap_orfor defaults. Provide a fallback when an error is acceptable.mustfor fatal errors. Crash with a clear message when recovery is impossible.safefor silencing. Suppress errors only when the operation is truly optional.
Type Conversions
Forge is dynamically typed and does not perform implicit type coercion between incompatible types (with the exception of Int to Float promotion in mixed arithmetic). Explicit conversion functions are provided for converting between types.
Conversion Functions
str(value) — Convert to String
Converts any value to its string representation.
say str(42) // "42"
say str(3.14) // "3.14"
say str(true) // "true"
say str(null) // "null"
say str([1, 2]) // "[1, 2]"
str() never fails. Every Forge value has a string representation.
int(value) — Convert to Int
Converts a value to a 64-bit signed integer.
say int("42") // 42
say int("100") // 100
say int(3.14) // 3 (truncates toward zero)
say int(true) // 1
say int(false) // 0
If the input string cannot be parsed as an integer, int() produces a runtime error. Always validate user input before converting.
| Input Type | Behavior |
|---|---|
String | Parses decimal integer; error if invalid |
Float | Truncates toward zero |
Bool | true = 1, false = 0 |
Int | Returns the value unchanged |
| Other | Runtime error |
float(value) — Convert to Float
Converts a value to a 64-bit floating-point number.
say float("3.14") // 3.14
say float(42) // 42.0
say float(true) // 1.0
say float(false) // 0.0
If the input string cannot be parsed as a float, float() produces a runtime error.
| Input Type | Behavior |
|---|---|
String | Parses decimal float; error if invalid |
Int | Promotes to float (lossless for most values) |
Bool | true = 1.0, false = 0.0 |
Float | Returns the value unchanged |
| Other | Runtime error |
Type Inspection Functions
typeof(value) — Get Type Name
Returns a string describing the runtime type of a value.
say typeof(42) // Int
say typeof(3.14) // Float
say typeof("hello") // String
say typeof(true) // Bool
say typeof(null) // Null
say typeof([1, 2, 3]) // Array
say typeof({ name: "Alice" }) // Object
For struct instances, typeof() returns the struct name:
thing Point { x: Int, y: Int }
let p = Point { x: 1, y: 2 }
say typeof(p) // Point
For functions:
fn f() { return 1 }
say typeof(f) // Function
type(value) — Alias for typeof
The type() function is an alias for typeof(). Both return identical results:
let value = 3.14
if type(value) == "Float" {
say "It's a float"
}
Implicit Conversions
Forge performs very few implicit conversions:
Int-to-Float Promotion
When an arithmetic operator has one Int operand and one Float operand, the integer is implicitly promoted to a float. The result is a Float:
say 5 + 2.0 // 7.0 (Int promoted to Float)
say 10 / 3.0 // 3.3333333333333335
String Interpolation
Inside string interpolation ({expr}), the expression result is implicitly converted to a string using the same logic as str():
let n = 42
say "The answer is {n}" // "The answer is 42"
Truthiness
When a value is used in a boolean context (e.g., if condition), it is evaluated for truthiness (see Types). This is not a type conversion — the value itself is not changed. It is a contextual interpretation.
No Other Implicit Coercion
Operations between incompatible types (e.g., adding a string and an integer) produce a runtime error. Explicit conversion is required:
// Error: cannot add String and Int
// say "age: " + 30
// Correct: convert explicitly
say "age: " + str(30)
Expressions
An expression is a syntactic construct that evaluates to a value. Every expression in Forge has a type and produces a result when evaluated.
Forge distinguishes expressions from statements: expressions produce values, statements produce effects. An expression can appear anywhere a value is expected – as the right-hand side of a variable binding, as a function argument, or as the body of a when arm.
Expression Categories
Literal Expressions
Literal expressions produce values directly from source text.
| Literal | Example | Value Type |
|---|---|---|
| Integer | 42 | int |
| Float | 3.14 | float |
| String | "hello" | string |
| Boolean | true, false | bool |
| Null | null | null |
| Array | [1, 2, 3] | array |
| Object | { name: "Forge", version: 1 } | object |
String literals support interpolation with embedded expressions.
Identifier Expressions
An identifier evaluates to the value bound to that name in the current scope.
let x = 10
say x // evaluates to 10
If the identifier is not in scope, evaluation produces a runtime error.
Arithmetic Expressions
Binary operations on numeric values: +, -, *, /, %. The + operator also performs string concatenation.
See Arithmetic.
Comparison and Logical Expressions
Comparison operators (==, !=, <, >, <=, >=) produce boolean values. Logical operators (and/&&, or/||, not/!) combine boolean expressions with short-circuit evaluation.
Field Access Expressions
Dot notation accesses fields on objects and struct instances. Embedded fields are resolved through delegation.
let user = { name: "Alice", age: 30 }
say user.name // "Alice"
See Field Access.
Index Expressions
Bracket notation accesses elements by position in arrays or by key in objects.
let items = [10, 20, 30]
say items[0] // 10
let obj = { x: 1 }
say obj["x"] // 1
Method Call Expressions
Method calls use dot notation followed by a function call. Forge resolves methods through a multi-step dispatch chain: object fields, type method tables, embedded field delegation, then known built-in methods.
let names = ["Charlie", "Alice", "Bob"]
say names.sort() // ["Alice", "Bob", "Charlie"]
See Method Calls.
Function Call Expressions
A function call evaluates a callable expression and applies it to a list of argument expressions.
fn square(n) { n * n }
say square(5) // 25
Closures and Lambdas
Anonymous functions that capture their enclosing environment.
let double = fn(x) { x * 2 }
say double(5) // 10
See Closures and Lambdas.
When Guard Expressions
Pattern-matching on a scrutinee value using comparison operators.
let label = when age {
< 13 -> "child",
< 18 -> "teen",
else -> "adult"
}
See When Guards.
Match Expressions
Structural pattern matching with destructuring of algebraic data types.
match shape {
Circle(r) => say "radius: {r}",
Rect(w, h) => say "area: {w * h}",
_ => say "unknown"
}
See Match Expressions.
String Interpolation Expressions
Double-quoted strings with embedded expressions in {...} delimiters.
let name = "world"
say "hello, {name}!" // "hello, world!"
See String Interpolation.
Pipeline Expressions
The pipe operator |> threads a value through a chain of function calls.
[1, 2, 3, 4, 5]
|> filter(fn(x) { x > 2 })
|> map(fn(x) { x * 10 })
Unary Expressions
Unary operators: - (numeric negation) and not/! (logical negation).
let x = -5
let flag = not true
Try Expressions
The ? operator propagates errors from Result values. If the value is Err, evaluation returns early from the enclosing function.
let data = fs.read("config.json")?
Must Expressions
The must keyword unwraps a Result or crashes with a descriptive error message.
let data = must fs.read("config.json")
Struct Initialization Expressions
Creates an instance of a named struct type.
thing Point { x: int, y: int }
let p = Point { x: 10, y: 20 }
Spread Expressions
The ... operator expands an array or object in a literal context.
let base = [1, 2, 3]
let extended = [...base, 4, 5]
Expression Evaluation Order
Forge evaluates expressions left to right. In a binary expression a + b, a is evaluated before b. In a function call f(x, y), f is evaluated first, then x, then y.
Logical operators and/&& and or/|| use short-circuit evaluation: the right operand is not evaluated if the left operand determines the result. See Comparison and Logical for details.
Arithmetic Expressions
Arithmetic expressions perform numeric computations and string concatenation.
Operators
| Operator | Name | Operand Types | Result Type |
|---|---|---|---|
+ | Addition | int + int | int |
+ | Addition | float + float | float |
+ | Addition | int + float or float + int | float |
+ | Concatenation | string + string | string |
- | Subtraction | int - int | int |
- | Subtraction | float - float | float |
- | Subtraction | int - float or float - int | float |
* | Multiplication | int * int | int |
* | Multiplication | float * float | float |
* | Multiplication | int * float or float - int | float |
/ | Division | int / int | int (truncated) |
/ | Division | float / float | float |
/ | Division | int / float or float / int | float |
% | Modulo | int % int | int |
% | Modulo | float % float | float |
Integer Arithmetic
Integer arithmetic uses 64-bit signed integers (i64). Operations that overflow follow Rust’s default behavior (panic in debug, wrap in release).
let a = 10 + 3 // 13
let b = 10 - 3 // 7
let c = 10 * 3 // 30
let d = 10 / 3 // 3 (truncated toward zero)
let e = 10 % 3 // 1
Integer Division
Integer division truncates toward zero. The result is always an integer when both operands are integers.
say 7 / 2 // 3
say -7 / 2 // -3
say 7 / -2 // -3
Float Arithmetic
Float arithmetic uses 64-bit IEEE 754 double-precision floating-point numbers (f64).
let a = 3.14 + 2.0 // 5.14
let b = 10.0 / 3.0 // 3.3333333333333335
let c = 2.5 % 1.0 // 0.5
Mixed Arithmetic
When one operand is int and the other is float, the integer is promoted to a float before the operation. The result is always float.
say 5 + 2.0 // 7.0
say 10 / 3.0 // 3.3333333333333335
say 3 * 1.5 // 4.5
String Concatenation
The + operator concatenates two strings. If one operand is a string and the other is not, the non-string operand is converted to its string representation.
say "hello" + " " + "world" // "hello world"
say "count: " + str(42) // "count: 42"
For embedding expressions in strings, prefer string interpolation:
say "count: {42}" // "count: 42"
Unary Negation
The - prefix operator negates a numeric value.
let x = 5
say -x // -5
say -3.14 // -3.14
Operator Precedence
Arithmetic operators follow standard mathematical precedence:
- Unary
-(highest) *,/,%+,-(lowest among arithmetic)
Parentheses override precedence:
say 2 + 3 * 4 // 14
say (2 + 3) * 4 // 20
See the Operator Precedence appendix for the full precedence table including all operator categories.
Division by Zero
Dividing by zero produces a runtime error:
say 10 / 0 // runtime error: division by zero
Float division by zero follows IEEE 754 rules and may produce infinity or NaN.
Comparison and Logical Expressions
Comparison operators produce boolean values by comparing two operands. Logical operators combine boolean expressions using short-circuit evaluation.
Comparison Operators
| Operator | Meaning | Example |
|---|---|---|
== | Equal | x == y |
!= | Not equal | x != y |
< | Less than | x < y |
> | Greater than | x > y |
<= | Less than or equal | x <= y |
>= | Greater than or equal | x >= y |
Equality
The == operator tests structural equality. Two values are equal if they have the same type and the same content.
say 1 == 1 // true
say "abc" == "abc" // true
say [1, 2] == [1, 2] // true
say null == null // true
say 1 == 1.0 // true (numeric promotion)
say 1 == "1" // false (different types)
Numeric Comparison
Integers and floats can be compared directly. When comparing an int with a float, the integer is promoted to float.
say 3 < 5 // true
say 3.14 > 2.71 // true
say 10 >= 10 // true
say 5 <= 3 // false
say 1 < 2.5 // true (int promoted to float)
String Comparison
Strings are compared lexicographically (by Unicode code points).
say "apple" < "banana" // true
say "abc" == "abc" // true
say "a" < "b" // true
Null Comparison
Only null is equal to null. Comparing null with any other value using == yields false.
say null == null // true
say null == 0 // false
say null == "" // false
say null == false // false
Logical Operators
Forge supports two syntactic forms for each logical operator. Both forms are equivalent.
| Classic | Natural | Meaning |
|---|---|---|
&& | and | Logical AND |
|| | or | Logical OR |
! | not | Logical NOT |
Logical AND
Returns true if both operands are truthy. Uses short-circuit evaluation: if the left operand is falsy, the right operand is not evaluated.
say true and true // true
say true and false // false
say false and true // false (right side not evaluated)
Logical OR
Returns true if either operand is truthy. Uses short-circuit evaluation: if the left operand is truthy, the right operand is not evaluated.
say false or true // true
say true or false // true (right side not evaluated)
say false or false // false
Logical NOT
Returns the boolean negation of the operand. Applies truthiness rules.
say not true // false
say not false // true
say !null // true
say not 0 // false (0 is truthy in Forge)
Short-Circuit Evaluation
Short-circuit evaluation means the right operand of and/&& or or/|| is only evaluated when necessary.
// The function is never called because the left side is false
false and expensive_computation()
// The function is never called because the left side is true
true or expensive_computation()
This is significant when the right operand has side effects:
let x = null
// Safe: the right side is not evaluated when x is null
x != null and x.name == "Alice"
Truthiness Rules
Forge uses the following truthiness rules when a value appears in a boolean context (such as an if condition or a logical operator):
| Value | Truthiness |
|---|---|
false | falsy |
null | falsy |
| Everything else | truthy |
Notably, the following values are truthy (unlike some other languages):
0(zero)""(empty string)[](empty array){}(empty object)
if 0 {
say "zero is truthy" // this executes
}
if "" {
say "empty string is truthy" // this executes
}
if null {
say "unreachable"
} otherwise {
say "null is falsy" // this executes
}
Operator Precedence
From highest to lowest precedence:
not/!(unary)<,>,<=,>===,!=and/&&or/||
say true or false and false // true (and binds tighter than or)
say not false and true // true (not binds tightest)
See the Operator Precedence appendix for the full table.
String Interpolation
Double-quoted string literals in Forge support interpolation: embedding arbitrary expressions inside { and } delimiters. The expressions are evaluated at runtime and their results are converted to string representations and spliced into the surrounding text.
Syntax
" ... {expression} ... "
Any valid Forge expression may appear between the braces.
Basic Usage
let name = "Forge"
say "Hello, {name}!" // "Hello, Forge!"
let x = 10
let y = 20
say "{x} + {y} = {x + y}" // "10 + 20 = 30"
Arbitrary Expressions
The interpolated expression is not limited to identifiers. Any expression that produces a value is permitted, including function calls, arithmetic, method calls, and field access.
say "length: {len([1, 2, 3])}" // "length: 3"
say "upper: {"hello".upper()}" // "upper: HELLO"
say "sum: {1 + 2 + 3}" // "sum: 6"
say "type: {typeof(42)}" // "type: int"
Value Conversion
Interpolated values are converted to their string representation using the same rules as the str() builtin:
| Value Type | String Representation |
|---|---|
string | The string itself |
int | Decimal representation (e.g., "42") |
float | Decimal representation (e.g., "3.14") |
bool | "true" or "false" |
null | "null" |
array | "[1, 2, 3]" |
object | "{key: value, ...}" |
Escape Sequences
The following escape sequences are recognized inside double-quoted strings:
| Escape | Character |
|---|---|
\n | Newline |
\t | Tab |
\\ | Backslash |
\{ | Literal { (suppresses interpolation) |
\" | Double quote |
To include a literal { character without triggering interpolation, escape it:
say "use \{braces\} for interpolation"
// Output: use {braces} for interpolation
Nesting
Interpolated expressions may themselves contain string literals with interpolation, though this is discouraged for readability:
let items = ["a", "b", "c"]
say "result: {join(items, ", ")}"
Non-Interpolated Strings
Single-quoted strings do not support interpolation. Use single quotes when the string contains braces that should be treated literally:
say 'no {interpolation} here'
// Output: no {interpolation} here
Empty Interpolation
An empty interpolation {} is not valid and produces a parse error.
Implementation Notes
String interpolation is parsed into a StringInterp AST node containing a sequence of StringPart elements, each being either a Literal (raw text) or an Expr (an evaluated expression). At runtime, all parts are evaluated and concatenated to produce the final string value.
Field Access
The dot operator (.) accesses fields on objects and struct instances. Forge resolves field access through direct lookup followed by delegation through embedded fields.
Syntax
expression.identifier
The left operand is evaluated to produce an object. The identifier names the field to retrieve.
Object Field Access
Objects are unordered collections of key-value pairs. Dot notation retrieves a value by key.
let user = { name: "Alice", age: 30 }
say user.name // "Alice"
say user.age // 30
If the field does not exist on the object, a runtime error is produced:
let user = { name: "Alice" }
say user.email // runtime error: no field 'email' on object
Struct Field Access
Struct instances (created from thing/struct definitions) are objects with a __type__ field. Field access works identically.
thing Point { x: int, y: int }
let p = Point { x: 10, y: 20 }
say p.x // 10
say p.y // 20
Embedded Field Delegation
When a struct uses has to embed another type, field access delegates to the embedded field if the field is not found directly on the outer object.
thing Base { id: int }
thing Extended {
has base: Base
name: string
}
let e = Extended { base: Base { id: 1 }, name: "test" }
say e.name // "test" (direct)
say e.id // 1 (delegated to e.base.id)
Resolution Order
Field access resolves in this order:
- Direct field lookup: Check if the object has a field with the given name.
- Embedded field delegation: If the object has a
__type__, check each embedded field’s sub-object for the field name. Embedded fields are checked in definition order.
If neither step finds the field, a runtime error is produced.
Chaining
Field access expressions can be chained to traverse nested structures.
let config = {
server: {
host: "localhost",
port: 8080
}
}
say config.server.host // "localhost"
say config.server.port // 8080
Built-in Field Access on Primitives
Certain built-in fields are available on primitive types:
Strings
| Field | Type | Description |
|---|---|---|
.len | int | Number of bytes |
.upper | string | Uppercase copy |
.lower | string | Lowercase copy |
.trim | string | Whitespace-trimmed copy |
let s = " Hello "
say s.len // 9
say s.upper // " HELLO "
say s.trim // "Hello"
Arrays
| Field | Type | Description |
|---|---|---|
.len | int | Number of elements |
let items = [1, 2, 3]
say items.len // 3
Index-Based Access
For dynamic key access, use bracket notation instead of dot notation:
let obj = { name: "Alice" }
let key = "name"
say obj[key] // "Alice"
Bracket notation works on both objects (with string keys) and arrays (with integer indices).
Method Calls
A method call uses dot notation to invoke a function on a receiver value. Forge resolves method calls through a multi-step dispatch process.
Syntax
expression.method(arguments)
The left operand (the receiver) is evaluated first, then the method is resolved, then arguments are evaluated left to right.
Method Dispatch
When evaluating obj.method(args), the interpreter follows this resolution order:
1. Object Field Lookup
If the receiver is an object and has a field named method whose value is callable (a function or closure), that field is invoked directly.
let obj = {
greet: fn(name) { "hello, {name}" }
}
say obj.greet("world") // "hello, world"
2. Static Method Lookup
If the receiver is a struct type reference (not an instance), static methods registered via give/impl blocks are checked.
thing Counter { value: int }
give Counter {
fn new() {
Counter { value: 0 }
}
}
let c = Counter.new() // static method call
3. Instance Method Lookup (method_tables)
If the receiver is a typed object (has a __type__ field), the interpreter looks up the type name in the global method table. Methods registered via give/impl blocks are found here. The receiver is automatically passed as the first argument (self).
thing Circle { radius: float }
give Circle {
fn area(self) {
3.14159 * self.radius * self.radius
}
}
let c = Circle { radius: 5.0 }
say c.area() // 78.53975
4. Embedded Field Delegation
If the receiver is a typed object and no method is found in step 3, the interpreter checks embedded fields (declared with has). For each embedded field, the interpreter looks up the embedded type’s method table. If a match is found, the embedded sub-object is passed as self instead of the outer object.
thing Animal { name: string }
give Animal {
fn speak(self) { "{self.name} speaks" }
}
thing Pet {
has animal: Animal
owner: string
}
let p = Pet { animal: Animal { name: "Rex" }, owner: "Alice" }
say p.speak() // "Rex speaks" (delegated to Animal.speak)
5. Built-in String Methods
Strings have a small set of built-in methods that are resolved directly without going through the method table:
| Method | Return Type | Description |
|---|---|---|
.upper() | string | Uppercase copy |
.lower() | string | Lowercase copy |
.trim() | string | Trimmed copy |
.len() | int | Byte length |
.chars() | array | Array of single-character strings |
say "hello".upper() // "HELLO"
say " hi ".trim() // "hi"
say "abc".chars() // ["a", "b", "c"]
6. Known Built-in Methods
If none of the above steps match, Forge checks a set of known built-in method names. If the method name matches, the call is rewritten as a function call with the receiver prepended to the argument list: obj.method(args) becomes method(obj, args).
This allows calling built-in functions with method syntax:
let nums = [3, 1, 2]
say nums.sort() // [1, 2, 3]
say nums.map(fn(x) { x * 2 }) // [6, 2, 4]
say nums.filter(fn(x) { x > 1 }) // [3, 2]
let text = "hello world"
say text.split(" ") // ["hello", "world"]
say text.starts_with("hello") // true
The full list of known built-in methods includes: map, filter, reduce, sort, reverse, push, pop, len, contains, keys, values, enumerate, split, join, replace, find, flat_map, has_key, get, pick, omit, merge, entries, from_entries, starts_with, ends_with, upper, lower, trim, substring, index_of, last_index_of, pad_start, pad_end, capitalize, title, repeat_str, count, sum, min_of, max_of, any, all, unique, zip, flatten, group_by, chunk, slice, slugify, snake_case, camel_case, sample, shuffle, partition, diff.
Self Parameter
Methods defined in give/impl blocks receive the receiver as their first argument, conventionally named self. This parameter must be declared explicitly.
thing Rect { w: int, h: int }
give Rect {
fn area(self) {
self.w * self.h
}
fn scale(self, factor) {
Rect { w: self.w * factor, h: self.h * factor }
}
}
Method Chaining
Method calls can be chained. Each call in the chain returns a value that becomes the receiver for the next call.
let result = [5, 2, 8, 1, 9]
.sort()
.filter(fn(x) { x > 3 })
.map(fn(x) { x * 10 })
// result: [50, 80, 90]
Resolution Failure
If method resolution exhausts all steps without finding a match, a runtime error is produced:
let x = 42
say x.nonexistent() // runtime error: unknown method 'nonexistent'
Closures and Lambdas
A closure (also called a lambda) is an anonymous function expression that captures variables from its enclosing scope. Closures are first-class values: they can be stored in variables, passed as arguments, and returned from functions.
Syntax
fn(parameters) { body }
The keyword fn introduces a closure. Parameters are comma-separated identifiers enclosed in parentheses. The body is a block of statements.
let double = fn(x) { x * 2 }
say double(5) // 10
No-Parameter Closures
Closures with no parameters use empty parentheses:
let greet = fn() { "hello" }
say greet() // "hello"
Implicit Return
The last expression in a closure body is its return value. An explicit return statement is also permitted but rarely needed.
let add = fn(a, b) { a + b } // implicit return
say add(3, 4) // 7
let abs = fn(x) {
if x < 0 {
return -x // explicit return
}
x
}
Capture Semantics
Closures capture variables from their enclosing scope by reference. Changes to captured variables are visible inside the closure, and mutations inside the closure affect the outer scope.
let mut count = 0
let increment = fn() {
count = count + 1
count
}
say increment() // 1
say increment() // 2
say count // 2
Capture at Definition Time
The closure captures a reference to the variable’s binding, not its current value. The variable is resolved at the time the closure is called, not when it is defined.
let mut x = 10
let get_x = fn() { x }
say get_x() // 10
x = 20
say get_x() // 20
Higher-Order Functions
Closures enable higher-order programming patterns. A higher-order function either takes a function as an argument or returns one.
Closures as Arguments
Many built-in functions accept closures:
let nums = [1, 2, 3, 4, 5]
let evens = filter(nums, fn(x) { x % 2 == 0 })
say evens // [2, 4]
let doubled = map(nums, fn(x) { x * 2 })
say doubled // [2, 4, 6, 8, 10]
let total = reduce(nums, 0, fn(acc, x) { acc + x })
say total // 15
Returning Closures
Functions can return closures, creating function factories:
fn make_adder(n) {
fn(x) { x + n }
}
let add5 = make_adder(5)
say add5(10) // 15
say add5(20) // 25
Closures in Method Syntax
Closures integrate naturally with method-style calls:
let names = ["Charlie", "Alice", "Bob"]
let sorted = names.sort(fn(a, b) { a < b })
say sorted // ["Alice", "Bob", "Charlie"]
Multi-Statement Bodies
Closure bodies can contain multiple statements. The last expression is the return value.
let process = fn(items) {
let filtered = filter(items, fn(x) { x > 0 })
let doubled = map(filtered, fn(x) { x * 2 })
doubled
}
say process([-1, 2, -3, 4]) // [4, 8]
Closures vs Named Functions
Closures and named functions (fn name(...) { } / define name(...) { }) differ in two ways:
- Naming: Named functions are bound to a name in the current scope. Closures are anonymous and must be assigned to a variable explicitly.
- Hoisting: Named functions are available throughout their defining scope. Closures are only available after the variable assignment that holds them.
Both named functions and closures capture their environment identically.
Recursive Closures
A closure can call itself recursively if it is assigned to a variable that is in scope at the time of the call:
let factorial = fn(n) {
if n <= 1 { 1 }
else { n * factorial(n - 1) }
}
say factorial(5) // 120
Type Annotations
Closures do not currently support parameter or return type annotations. The types of parameters and the return value are inferred at runtime.
When Guards
A when expression performs multi-way branching on a scrutinee value using comparison operators. Each arm specifies a comparison operation applied to the scrutinee; the first matching arm’s result expression is returned.
Syntax
when expression {
op value -> result,
op value -> result,
else -> result
}
The scrutinee is the expression after when. Each arm consists of a comparison operator, a value to compare against, the -> arrow, and a result expression. The optional else arm matches when no other arm does.
Basic Usage
let label = when age {
< 13 -> "child",
< 18 -> "teen",
< 65 -> "adult",
else -> "senior"
}
say label
The scrutinee age is evaluated once. Each arm’s operator and value are applied to the scrutinee in order. The first arm whose comparison returns true provides the result.
Comparison Operators in Arms
Arms support any comparison operator:
| Operator | Meaning |
|---|---|
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
== | Equal |
!= | Not equal |
let status = when code {
== 200 -> "ok",
== 404 -> "not found",
== 500 -> "server error",
>= 400 -> "client error",
else -> "unknown"
}
Evaluation Semantics
- The scrutinee expression is evaluated exactly once.
- Arms are tested top to bottom.
- For each arm, the arm’s comparison operator is applied with the scrutinee as the left operand and the arm’s value as the right operand.
- The first arm that produces
truedetermines the result: its result expression is evaluated and returned. - If no arm matches and an
elsearm is present, theelseresult is returned. - If no arm matches and no
elsearm is present, the when expression evaluates tonull.
When as an Expression
when produces a value and can be used anywhere an expression is expected:
say when score {
>= 90 -> "A",
>= 80 -> "B",
>= 70 -> "C",
else -> "F"
}
let discount = when items {
> 100 -> 0.20,
> 50 -> 0.10,
> 10 -> 0.05,
else -> 0.0
}
When as a Statement
when can also appear at the statement level:
when temperature {
> 100 -> say "boiling",
< 0 -> say "freezing",
else -> say "normal"
}
Arm Result Expressions
Each arm’s result is a single expression. For multi-statement logic, use a block expression or call a function:
let result = when level {
> 10 -> {
let bonus = level * 2
bonus + 100
},
else -> 0
}
The else Arm
The else arm is a catch-all that matches when no other arm does. It must be the last arm if present.
let kind = when x {
> 0 -> "positive",
< 0 -> "negative",
else -> "zero"
}
If no else arm is provided and no arm matches, the when expression evaluates to null.
Differences from Match
when guards and match expressions serve different purposes:
| Feature | when | match |
|---|---|---|
| Comparison style | Operator-based guards | Structural pattern matching |
| Scrutinee | Compared via operators | Destructured via patterns |
| Use case | Numeric/comparable ranges | ADT variant matching |
See Match Expressions for structural pattern matching.
Match Expressions
A match expression performs structural pattern matching on a value. Each arm specifies a pattern; the first arm whose pattern matches the scrutinee has its body evaluated.
Syntax
match expression {
pattern => body,
pattern => body,
...
}
The scrutinee is the expression after match. Each arm consists of a pattern, the => arrow, and a body (one or more statements). Arms are separated by commas.
Patterns
Forge supports the following pattern forms:
Wildcard Pattern
The underscore _ matches any value and binds nothing.
match x {
_ => say "matched anything"
}
Literal Pattern
A literal value matches if the scrutinee is equal to that value.
match color {
"red" => say "stop",
"green" => say "go",
"yellow" => say "caution",
_ => say "unknown"
}
Binding Pattern
A bare identifier binds the matched value to that name within the arm body.
match value {
x => say "got: {x}"
}
Constructor Pattern
A constructor pattern matches an algebraic data type (ADT) variant and destructures its fields. The pattern names the variant and provides sub-patterns for each field.
type Shape {
Circle(float)
Rect(float, float)
Point
}
let s = Circle(5.0)
match s {
Circle(r) => say "circle with radius {r}",
Rect(w, h) => say "rectangle {w} x {h}",
Point => say "a point",
_ => say "unknown shape"
}
Nested constructor patterns are supported:
type Expr {
Num(int)
Add(Expr, Expr)
}
match expr {
Add(Num(a), Num(b)) => say "sum: {a + b}",
Num(n) => say "number: {n}",
_ => say "complex expression"
}
Evaluation Semantics
- The scrutinee expression is evaluated exactly once.
- Arms are tested top to bottom.
- For each arm, the pattern is matched against the scrutinee:
- Wildcard: Always matches.
- Literal: Matches if the scrutinee equals the literal value.
- Binding: Always matches; binds the scrutinee to the identifier.
- Constructor: Matches if the scrutinee is an ADT value with the same variant name and the correct number of fields, and all sub-patterns recursively match.
- The first matching arm’s body is evaluated. Bindings introduced by the pattern are in scope for the body.
- If no arm matches, the match expression evaluates to
null.
Match as an Expression
match produces a value and can be used in expression position:
let area = match shape {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
_ => 0.0
}
Match as a Statement
match can appear at the statement level:
match event {
Click(x, y) => handle_click(x, y),
KeyPress(key) => handle_key(key),
_ => {}
}
Multi-Statement Arm Bodies
Arm bodies can contain multiple statements. The last expression in the block is the value of the arm.
let result = match data {
Some(value) => {
let processed = transform(value)
validate(processed)
processed
},
None => default_value()
}
ADT Matching
Match is the primary mechanism for working with algebraic data types (see Algebraic Data Types).
type Result {
Ok(any)
Err(string)
}
fn handle(r) {
match r {
Ok(value) => say "success: {value}",
Err(msg) => say "error: {msg}"
}
}
Exhaustiveness
Forge does not currently enforce exhaustive matching. If no arm matches the scrutinee, the match expression evaluates to null. Use a wildcard _ arm as the final arm to ensure all cases are handled.
Differences from When Guards
| Feature | match | when |
|---|---|---|
| Arrow syntax | => | -> |
| Matching style | Structural patterns | Comparison operators |
| Destructuring | Yes (ADT variants) | No |
| Use case | ADT variants, literal dispatch | Numeric ranges, comparisons |
See When Guards for operator-based branching.
Statements
A statement is a syntactic construct that performs an action. Unlike expressions, statements do not produce values (with the exception of expression statements, where an expression is evaluated for its side effects and the result is discarded).
A Forge program is a sequence of statements executed top to bottom.
Statement Categories
Declarations
Declarations introduce new names into the current scope.
- Variable declaration:
let x = expr/set x to expr– binds a value to a name. See Variable Declaration. - Function declaration:
fn name(params) { body }/define name(params) { body }– binds a function to a name. See Function Declaration. - Destructuring declaration:
let {a, b} = obj/unpack {a, b} from obj– binds multiple names from a compound value. See Variable Declaration.
Assignments
Assignments change the value of an existing binding.
- Simple assignment:
x = expr/change x to expr - Compound assignment:
x += expr,x -= expr,x *= expr,x /= expr - Field assignment:
obj.field = expr - Index assignment:
arr[i] = expr
See Assignment.
Control Flow
Control flow statements direct the order of execution.
- Conditional:
if condition { body }with optionalelse/otherwise/nahclauses. See Control Flow. - When guards:
when expr { arms }– multi-way branch on a value. See When Guards. - Match:
match expr { arms }– structural pattern matching. See Match Expressions.
Loops
Loop statements execute a body repeatedly.
- For-in:
for item in collection { body }/for each item in collection { body } - While:
while condition { body } - Loop:
loop { body }– infinite loop, exit withbreak - Repeat:
repeat N times { body }– counted loop
See Loops.
Jump Statements
Jump statements transfer control to a different point in the program.
- return: Exits the current function, optionally with a value.
- break: Exits the innermost loop.
- continue: Skips to the next iteration of the innermost loop.
Module Statements
Module statements manage code organization across files.
- import:
import "file.fg"– executes another file and imports its definitions.
See Import and Export.
Expression Statements
Any expression can appear as a statement. The expression is evaluated and its result is discarded. This is how function calls with side effects are written.
say "hello" // function call as statement
push(items, 42) // side-effecting call
Statement Terminators
Forge does not require semicolons or other explicit statement terminators. Statements are separated by newlines. Multiple statements may appear on a single line if they are unambiguous to the parser.
let x = 10
let y = 20
say x + y
Blocks
A block is a sequence of statements enclosed in { and }. Blocks appear as the body of functions, loops, conditionals, and other compound statements. Blocks create a new scope: variables declared inside a block are not visible outside it.
let x = "outer"
{
let x = "inner"
say x // "inner"
}
say x // "outer"
Variable Declaration
Variable declarations introduce new bindings in the current scope. Forge supports both classic and natural syntax forms, with optional mutability and destructuring.
Immutable Variables
By default, variables are immutable. An immutable binding cannot be reassigned after initialization.
Classic Syntax
let name = "Alice"
let age = 30
let items = [1, 2, 3]
Natural Syntax
set name to "Alice"
set age to 30
set items to [1, 2, 3]
Both forms are semantically identical. The variable is bound to the result of evaluating the right-hand expression.
Mutable Variables
To allow a variable to be reassigned after its initial declaration, use the mut keyword.
Classic Syntax
let mut count = 0
count = count + 1 // allowed
Natural Syntax
set mut count to 0
change count to count + 1 // allowed
Attempting to reassign an immutable variable produces a runtime error:
let x = 10
x = 20 // runtime error: cannot reassign immutable variable 'x'
Type Annotations
Variable declarations may include an optional type annotation after the variable name:
let name: string = "Alice"
let age: int = 30
let ratio: float = 0.5
Type annotations are checked by the type checker when enabled. They do not affect runtime behavior in the interpreter.
Initializer Expressions
The right-hand side of a variable declaration is any valid expression:
let sum = 1 + 2 + 3
let greeting = "Hello, {name}!"
let data = fs.read("config.json")
let result = compute(x, y)
Every variable declaration requires an initializer. There is no uninitialized variable syntax.
Destructuring
Forge supports destructuring assignment for objects and arrays.
Object Destructuring
Classic Syntax
let person = { name: "Alice", age: 30, city: "NYC" }
let { name, age } = person
say name // "Alice"
say age // 30
Natural Syntax
let person = { name: "Alice", age: 30, city: "NYC" }
unpack { name, age } from person
say name // "Alice"
say age // 30
Object destructuring extracts the named fields from an object and binds them to variables with the same names.
Array Destructuring
let coords = [10, 20, 30]
let [x, y, z] = coords
say x // 10
say y // 20
say z // 30
Rest Pattern
Array destructuring supports a rest pattern to capture remaining elements:
let items = [1, 2, 3, 4, 5]
let [first, ...rest] = items
say first // 1
say rest // [2, 3, 4, 5]
Scope
Variables are scoped to the block in which they are declared. A variable declared inside an if body, loop body, or function body is not accessible outside that block.
if true {
let x = 42
say x // 42
}
// x is not accessible here
Inner scopes can shadow variables from outer scopes:
let x = "outer"
{
let x = "inner"
say x // "inner"
}
say x // "outer"
Multiple Declarations
Each let/set statement declares a single binding (or a destructuring pattern). To declare multiple variables, use separate statements:
let x = 1
let y = 2
let z = 3
Assignment
Assignment statements change the value of an existing mutable binding. The target must have been declared with mut (or let mut / set mut).
Simple Assignment
Classic Syntax
let mut x = 10
x = 20
say x // 20
Natural Syntax
set mut x to 10
change x to 20
say x // 20
Both forms evaluate the right-hand expression and store the result in the named variable.
Mutability Requirement
Only variables declared with mut can be reassigned. Attempting to assign to an immutable variable produces a runtime error:
let x = 10
x = 20 // runtime error: cannot reassign immutable variable 'x'
Compound Assignment
Compound assignment operators combine an arithmetic operation with assignment. The target must be mutable.
| Operator | Equivalent To |
|---|---|
x += y | x = x + y |
x -= y | x = x - y |
x *= y | x = x * y |
x /= y | x = x / y |
let mut count = 0
count += 1 // count is 1
count += 5 // count is 6
count *= 2 // count is 12
count -= 3 // count is 9
count /= 3 // count is 3
Compound assignment with += on strings performs concatenation:
let mut msg = "hello"
msg += " world"
say msg // "hello world"
Field Assignment
Fields on objects and struct instances can be assigned using dot notation:
let mut user = { name: "Alice", age: 30 }
user.age = 31
say user.age // 31
Nested field assignment is supported:
let mut config = { server: { port: 8080 } }
config.server.port = 3000
say config.server.port // 3000
Index Assignment
Array elements and object keys can be assigned using bracket notation:
let mut items = [10, 20, 30]
items[1] = 99
say items // [10, 99, 30]
let mut obj = { a: 1, b: 2 }
obj["a"] = 100
say obj.a // 100
Assignment Is Not an Expression
In Forge, assignment is a statement, not an expression. Assignment does not produce a value and cannot be used in expression position:
// This is NOT valid:
// let y = (x = 5)
// Use separate statements:
let mut x = 0
x = 5
let y = x
Evaluation Order
In an assignment target = expression, the right-hand expression is evaluated first, then the result is stored in the target location.
For compound assignment target += expression, the current value of the target is read, the operation is performed with the right-hand expression, and the result is stored back.
Control Flow
Control flow statements direct the order of execution based on conditions.
If Statements
The if statement executes a block of code when a condition is truthy.
if temperature > 100 {
say "boiling"
}
The condition is any expression. It is evaluated using Forge’s truthiness rules: false and null are falsy, everything else is truthy.
If-Else
An else clause provides an alternative block when the condition is falsy. Forge supports three equivalent keywords for the else clause:
Classic: else
if age >= 18 {
say "adult"
} else {
say "minor"
}
Natural: otherwise
if age >= 18 {
say "adult"
} otherwise {
say "minor"
}
Casual: nah
if age >= 18 {
say "adult"
} nah {
say "minor"
}
All three forms are semantically identical. The else block executes when the condition is falsy.
If-Else If Chains
Multiple conditions can be tested in sequence using else if (or otherwise if / nah if):
if score >= 90 {
say "A"
} else if score >= 80 {
say "B"
} else if score >= 70 {
say "C"
} else {
say "F"
}
Conditions are tested top to bottom. The first truthy condition’s block is executed. If no condition is truthy and an else clause is present, its block executes.
Nested If Statements
If statements can be nested arbitrarily:
if user != null {
if user.role == "admin" {
say "admin access"
} else {
say "regular access"
}
} else {
say "not logged in"
}
Block Scoping
Variables declared inside an if or else block are scoped to that block:
if true {
let msg = "inside"
say msg // "inside"
}
// msg is not accessible here
No Ternary Operator
Forge does not have a ternary conditional operator (condition ? a : b). Use an if-else statement or a when expression instead:
// Using when as a conditional expression
let label = when age {
>= 18 -> "adult",
else -> "minor"
}
If as a Statement
if is always a statement in Forge. It does not produce a value that can be used in expression position. To select between values conditionally, use a when expression.
Truthiness in Conditions
The condition in an if statement follows Forge’s truthiness rules:
if 0 {
say "zero is truthy" // this executes
}
if "" {
say "empty string is truthy" // this executes
}
if null {
say "unreachable"
} else {
say "null is falsy" // this executes
}
if false {
say "unreachable"
} else {
say "false is falsy" // this executes
}
See Truthiness Rules for the complete specification.
Loops
Loop statements execute a body repeatedly. Forge provides several loop forms for different iteration patterns.
For-In Loops
The for-in loop iterates over elements of a collection.
let names = ["Alice", "Bob", "Charlie"]
for name in names {
say "Hello, {name}"
}
For Each Variant
The for each form is equivalent to for-in:
for each name in names {
say "Hello, {name}"
}
Iterating Over Arrays
let nums = [10, 20, 30]
for n in nums {
say n
}
// Output: 10, 20, 30
Iterating Over Objects
When iterating over an object, the loop variable receives each key:
let user = { name: "Alice", age: 30 }
for key in user {
say "{key}: {user[key]}"
}
Iterating with Index
Use enumerate or a two-variable for loop to access both index and value:
let items = ["a", "b", "c"]
for i, item in items {
say "{i}: {item}"
}
// Output: 0: a, 1: b, 2: c
Iterating Over Ranges
The range function generates a sequence of integers:
for i in range(0, 5) {
say i
}
// Output: 0, 1, 2, 3, 4
range(start, end) produces integers from start (inclusive) to end (exclusive).
While Loops
The while loop executes as long as its condition is truthy.
let mut count = 0
while count < 5 {
say count
count += 1
}
// Output: 0, 1, 2, 3, 4
The condition is evaluated before each iteration. If the condition is falsy on the first check, the body never executes.
Loop (Infinite)
The loop keyword creates an infinite loop. Use break to exit.
let mut n = 0
loop {
if n >= 3 {
break
}
say n
n += 1
}
// Output: 0, 1, 2
Repeat N Times
The repeat loop executes a body a fixed number of times.
repeat 3 times {
say "hello"
}
// Output: hello, hello, hello
The count expression is evaluated once before the loop begins. The body executes exactly that many times.
Break
The break statement exits the innermost enclosing loop immediately.
for i in range(0, 100) {
if i == 5 {
break
}
say i
}
// Output: 0, 1, 2, 3, 4
break can be used with for, while, loop, and repeat.
Continue
The continue statement skips the rest of the current iteration and proceeds to the next one.
for i in range(0, 5) {
if i == 2 {
continue
}
say i
}
// Output: 0, 1, 3, 4
Nested Loops
Loops can be nested. break and continue apply to the innermost loop only.
for i in range(0, 3) {
for j in range(0, 3) {
if j == 1 {
break // exits inner loop only
}
say "{i},{j}"
}
}
// Output: 0,0 1,0 2,0
Loop Scope
Variables declared inside a loop body are scoped to each iteration:
for i in range(0, 3) {
let msg = "iteration {i}"
say msg
}
// msg is not accessible here
The loop variable itself (i in for i in ...) is scoped to the loop body.
Wait in Loops
The wait statement can be used inside loops to introduce delays:
repeat 3 times {
say "tick"
wait 1 second
}
Function Declaration
Function declarations introduce named, callable units of code. Forge supports both classic and natural syntax forms.
Basic Declaration
Classic Syntax
fn greet(name) {
say "Hello, {name}!"
}
Natural Syntax
define greet(name) {
say "Hello, {name}!"
}
Both forms are semantically identical. The function is bound to the given name in the current scope.
Parameters
Parameters are comma-separated identifiers enclosed in parentheses.
fn add(a, b) {
a + b
}
say add(3, 4) // 7
No Parameters
Functions with no parameters use empty parentheses:
fn hello() {
say "hello"
}
hello()
Default Parameters
Parameters can have default values. Default values are used when the caller does not provide an argument for that position.
fn greet(name, greeting = "Hello") {
say "{greeting}, {name}!"
}
greet("Alice") // "Hello, Alice!"
greet("Bob", "Hi") // "Hi, Bob!"
Default parameters must appear after all required parameters.
Variadic Parameters
Forge does not support variadic parameters (rest parameters). To accept a variable number of arguments, use an array parameter:
fn sum_all(numbers) {
reduce(numbers, 0, fn(acc, n) { acc + n })
}
say sum_all([1, 2, 3, 4]) // 10
Return Type Annotation
An optional return type annotation may follow the parameter list:
fn square(n: int): int {
n * n
}
Type annotations are checked by the type checker when enabled. They do not affect runtime behavior in the interpreter.
Return Values
Implicit Return
The last expression in a function body is its return value. This is the idiomatic way to return values in Forge.
fn double(x) {
x * 2
}
say double(5) // 10
Explicit Return
The return keyword exits the function immediately with a value:
fn abs(x) {
if x < 0 {
return -x
}
x
}
A bare return without a value returns null:
fn log_if_positive(x) {
if x <= 0 {
return
}
say "positive: {x}"
}
See Return, Break, Continue for details.
Function Scope
Functions create a new scope. Variables declared inside a function are not accessible outside it. Functions can access variables from their enclosing scope (closure behavior).
let multiplier = 10
fn scale(x) {
x * multiplier // accesses 'multiplier' from outer scope
}
say scale(5) // 50
Recursion
Functions can call themselves recursively:
fn factorial(n) {
if n <= 1 { return 1 }
n * factorial(n - 1)
}
say factorial(5) // 120
fn fib(n) {
if n <= 1 { return n }
fib(n - 1) + fib(n - 2)
}
say fib(10) // 55
Async Functions
Async functions are declared with async fn (classic) or forge (natural):
async fn fetch_data(url) {
let resp = await http.get(url)
resp.body
}
// Natural syntax
forge fetch_data(url) {
let resp = hold http.get(url)
resp.body
}
Async functions return a future that must be awaited with await / hold. See Async Functions.
Nested Functions
Functions can be declared inside other functions:
fn outer() {
fn inner() {
say "inside"
}
inner()
}
outer() // "inside"
Inner functions have access to the outer function’s scope.
Functions Are Values
Function declarations create values that can be stored in variables, passed as arguments, and returned from other functions:
fn add(a, b) { a + b }
fn sub(a, b) { a - b }
let ops = [add, sub]
say ops[0](10, 3) // 13
say ops[1](10, 3) // 7
Parameter Type Annotations
Parameters can include optional type annotations:
fn add(a: int, b: int): int {
a + b
}
These annotations are informational for the type checker and do not enforce types at runtime in the interpreter.
Return, Break, Continue
Jump statements transfer control to a different point in the program, interrupting the normal top-to-bottom execution flow.
Return
The return statement exits the current function and optionally provides a return value.
Return with a Value
fn square(x) {
return x * x
}
say square(5) // 25
Return without a Value
A bare return returns null from the function:
fn maybe_log(x) {
if x <= 0 {
return // returns null
}
say "value: {x}"
}
Implicit Return
The last expression in a function body is automatically used as the return value. Explicit return is only needed for early exit.
fn double(x) {
x * 2 // implicit return
}
say double(5) // 10
When using if-else as the last statement, the last expression in the executed branch becomes the return value:
fn abs(x) {
if x < 0 {
-x
} else {
x
}
}
say abs(-3) // 3
Return in Nested Blocks
return exits the enclosing function, not just the current block:
fn find_first_negative(items) {
for item in items {
if item < 0 {
return item // exits the function, not just the loop
}
}
null
}
say find_first_negative([1, 2, -3, 4]) // -3
Return from Closures
A return inside a closure exits the closure, not the outer function:
fn process(items) {
let results = map(items, fn(x) {
if x < 0 {
return 0 // exits the closure, not process()
}
x * 2
})
results
}
say process([1, -2, 3]) // [2, 0, 6]
Break
The break statement exits the innermost enclosing loop immediately. Execution continues with the first statement after the loop.
Break in For Loops
for i in range(0, 100) {
if i >= 5 {
break
}
say i
}
// Output: 0, 1, 2, 3, 4
say "done"
Break in While Loops
let mut n = 0
while true {
if n >= 3 {
break
}
say n
n += 1
}
// Output: 0, 1, 2
Break in Loop
The break statement is the only way to exit a loop (infinite loop):
let mut count = 0
loop {
count += 1
if count > 5 {
break
}
}
say count // 6
Break in Nested Loops
break only exits the innermost loop:
for i in range(0, 3) {
for j in range(0, 10) {
if j >= 2 {
break // exits inner loop only
}
say "{i},{j}"
}
// continues with next i
}
Break Outside a Loop
Using break outside of a loop produces a runtime error.
Continue
The continue statement skips the rest of the current loop iteration and proceeds to the next iteration.
Continue in For Loops
for i in range(0, 5) {
if i == 2 {
continue // skip i == 2
}
say i
}
// Output: 0, 1, 3, 4
Continue in While Loops
let mut i = 0
while i < 5 {
i += 1
if i % 2 == 0 {
continue // skip even numbers
}
say i
}
// Output: 1, 3, 5
Continue in Nested Loops
Like break, continue applies to the innermost loop:
for i in range(0, 3) {
for j in range(0, 3) {
if j == 1 {
continue // skips j == 1 in inner loop
}
say "{i},{j}"
}
}
// Output: 0,0 0,2 1,0 1,2 2,0 2,2
Continue Outside a Loop
Using continue outside of a loop produces a runtime error.
Summary
| Statement | Context | Effect |
|---|---|---|
return expr | Function | Exit function, return expr |
return | Function | Exit function, return null |
break | Loop | Exit innermost loop |
continue | Loop | Skip to next iteration of innermost loop |
Import and Export
The import statement loads definitions from external Forge source files or references built-in modules.
Importing Files
To import all top-level definitions from another Forge source file:
import "utils.fg"
The import path is a string literal specifying the file to load. The interpreter resolves the path relative to the current working directory and checks the following locations in order:
{path}– the exact path as given{path}.fg– with the.fgextension appendedforge_modules/{path}/main.fg– in theforge_modulesdirectory
If no file is found at any of these locations, a runtime error is produced.
Import Semantics
When a file is imported, the following steps occur:
- The source file is read and parsed.
- A new interpreter instance is created.
- The imported file is executed in the new interpreter.
- Top-level definitions (
fnandletbindings) are copied into the importing file’s scope.
Importantly, each import creates a fresh interpreter. Side effects in the imported file (such as printing) occur during import. The imported file does not share mutable state with the importing file.
// utils.fg
fn double(x) { x * 2 }
fn triple(x) { x * 3 }
let PI = 3.14159
// main.fg
import "utils.fg"
say double(5) // 10
say triple(5) // 15
say PI // 3.14159
Selective Imports
To import specific names from a file, list them after the path:
import { double, PI } from "utils.fg"
say double(5) // 10
say PI // 3.14159
// triple is NOT imported
Only the listed names are copied into the current scope.
Built-in Modules
Forge’s standard library modules (math, fs, io, crypto, db, pg, env, json, regex, log, term, http, csv, exec, time) are automatically available without import. Attempting to import a built-in module name produces an error with guidance:
import "math"
// Error: 'math' is a built-in module — it's already available. Use it directly: math.function()
Built-in modules are accessed via dot notation:
say math.sqrt(16) // 4.0
say math.pi // 3.141592653589793
let data = fs.read("file.txt")
Module-Level Execution
When a file is imported, all of its top-level statements execute. This includes not just declarations but also expression statements and side effects:
// setup.fg
say "setting up..." // prints during import
let config = { debug: true }
fn get_config() { config }
// main.fg
import "setup.fg" // prints "setting up..."
let c = get_config()
say c.debug // true
Circular Imports
Forge does not detect or prevent circular imports. A circular import chain will cause infinite recursion and a stack overflow. It is the programmer’s responsibility to avoid circular dependencies.
No Namespacing
Imported definitions are placed directly into the importing file’s scope. There is no namespace or module prefix for file imports. If two imported files define the same name, the later import overwrites the earlier one.
import "a.fg" // defines fn helper()
import "b.fg" // also defines fn helper() -- overwrites a.fg's version
No Export Keyword
Forge does not have an explicit export keyword. All top-level fn and let bindings in a file are automatically available for import by other files.
Re-Imports
Importing the same file multiple times re-executes it each time. There is no import caching or module singleton behavior for file imports.
Type System
This chapter defines the type system of Forge. Forge is dynamically typed at runtime but provides structural mechanisms for organizing data and behavior: struct definitions, method attachment, interface contracts, composition via embedding, and structural satisfaction checking.
Overview
Forge’s type system is built on five pillars:
- Struct definitions (
thing/struct) — Define named record types with typed fields and optional defaults. - Method blocks (
give/impl) — Attach instance and static methods to struct types after definition. - Interface contracts (
power/interface) — Declare behavioral contracts that types may fulfill. - Composition (
has) — Embed one struct inside another with automatic field and method delegation. - Structural satisfaction (
satisfies) — Test whether a value’s type fulfills an interface at runtime, regardless of explicit declaration.
Dynamic Foundation
All Forge values are represented at runtime by the Value enum. There is no compile-time type erasure or monomorphization. Type annotations on struct fields and function parameters are documentation and future-proofing; the interpreter does not enforce them at assignment time in the current version.
The typeof builtin returns a string naming the runtime type:
typeof(42) // "Int"
typeof("hello") // "String"
typeof(true) // "Bool"
typeof(null) // "Null"
typeof([1, 2]) // "Array"
typeof({a: 1}) // "Object"
Struct instances are Object values with a __type__ field that records the struct name.
Type Identity
Every struct instance carries a __type__ field set to the struct’s name as a string. This field is automatically inserted during construction and is used by the runtime for method dispatch, interface satisfaction checking, and embedded-field delegation.
thing Point { x: Int, y: Int }
let p = Point { x: 1, y: 2 }
p.__type__ // "Point"
typeof(p) // "Object"
The typeof builtin returns "Object" for all struct instances. The __type__ field distinguishes between different struct types at a finer granularity.
No Generics
Forge does not currently support generic types or parameterized type constructors. All collections (arrays, objects) are heterogeneous. Interface methods are checked by name and arity, not by parameter types.
Subsections
The following subsections define each type system feature in detail:
- Struct Definitions — Defining named record types.
- Method Blocks — Attaching methods to types.
- Interface Contracts — Declaring and implementing behavioral contracts.
- Composition — Embedding types and delegation.
- Structural Satisfaction — Runtime interface checking.
- Default Values — Default field values in struct definitions.
- Static Methods — Type-level methods without a receiver.
Struct Definitions
Structs are named record types that group related fields into a single value. Forge provides dual syntax for defining structs: thing (natural) and struct (classic).
Syntax
StructDef = ("thing" | "struct") Identifier "{" FieldList "}"
FieldList = Field ("," Field)* ","?
Field = EmbedField | PlainField
PlainField = Identifier (":" TypeAnnotation)? ("=" Expression)?
EmbedField = "has" Identifier ":" TypeAnnotation
TypeAnnotation = Identifier
Defining a Struct
The thing keyword (or its classic alias struct) introduces a new struct type. The struct name must be a valid identifier and by convention uses PascalCase.
thing Person {
name: String,
age: Int
}
Classic syntax:
struct Person {
name: String,
age: Int
}
Both forms are semantically identical. The parser produces the same StructDef AST node regardless of which keyword is used.
Fields
Each field has a name and an optional type annotation. Type annotations follow the field name after a colon. In the current implementation, type annotations are parsed and stored but not enforced at runtime.
thing Config {
host: String,
port: Int,
debug: Bool
}
Fields without type annotations are permitted:
thing Pair {
first,
second
}
Default Values
Fields may specify a default value using = after the type annotation (or after the field name if no annotation is present). See Default Values for full details.
thing Config {
host: String = "localhost",
port: Int = 8080,
debug: Bool = false
}
Construction
Struct instances are created using the struct name followed by a field initializer block. The result is an Object value with the specified fields plus an automatically inserted __type__ field.
thing Point { x: Int, y: Int }
let p = Point { x: 10, y: 20 }
// p is { __type__: "Point", x: 10, y: 20 }
Fields may be provided in any order. Fields with default values may be omitted; default values are applied first, then explicitly provided fields override them.
thing Config {
host: String = "localhost",
port: Int = 8080
}
let c = Config { port: 3000 }
// c is { __type__: "Config", host: "localhost", port: 3000 }
The __type__ Field
Every struct instance automatically receives a __type__ field set to the struct’s name as a String value. This field is inserted after all user-specified fields during construction. It is used by the runtime for:
- Method dispatch in
give/implblocks - Interface satisfaction checking in
satisfies - Embedded field delegation via
has
The __type__ field is a regular field and can be read like any other:
thing Dog { name: String }
let d = Dog { name: "Rex" }
say d.__type__ // "Dog"
Manually setting __type__ in the constructor is permitted but will be overwritten by the automatic insertion.
Registration
When a StructDef statement is executed, the interpreter:
- Registers the struct name in the environment as a
BuiltIn("struct:Name")sentinel value. This value is used to identify static method calls (Name.method()). - Records any embedded fields in the
embedded_fieldstable for delegation. - Records any default values in the
struct_defaultstable, evaluating default expressions at definition time.
The struct name can subsequently be used as a constructor in StructInit expressions.
Field Access
Fields are accessed using dot notation:
thing Person { name: String, age: Int }
let p = Person { name: "Alice", age: 30 }
say p.name // "Alice"
say p.age // 30
If the field does not exist on the object directly, the runtime checks embedded sub-objects before reporting an error. See Composition.
Method Blocks
Method blocks attach functions to a struct type after its definition. Forge provides dual syntax: give (natural) and impl (classic). Methods are stored in the interpreter’s method tables and dispatched based on the instance’s __type__ field.
Syntax
MethodBlock = GiveBlock | ImplBlock
GiveBlock = "give" TypeName AbilityClause? "{" MethodDef* "}"
ImplBlock = "impl" (TypeName | AbilityForType) "{" MethodDef* "}"
AbilityClause = "the" "power" InterfaceName
AbilityForType = InterfaceName "for" TypeName
MethodDef = ("fn" | "define") Identifier "(" ParamList ")" Block
TypeName = Identifier
InterfaceName = Identifier
Instance Methods
An instance method is a method whose first parameter is named it. The it parameter receives the struct instance on which the method is called — it is Forge’s equivalent of self or this in other languages.
thing Person {
name: String,
age: Int
}
give Person {
define greet(it) {
say "Hello, I'm " + it.name
}
define birthday(it) {
return it.age + 1
}
}
Classic syntax:
impl Person {
fn greet(it) {
say "Hello, I'm " + it.name
}
}
Both give and impl are semantically identical. The parser produces the same ImplBlock AST node.
Method Invocation
Instance methods are called using dot notation on a struct instance. The runtime automatically passes the instance as the it argument:
let p = Person { name: "Alice", age: 30 }
p.greet() // prints "Hello, I'm Alice"
p.birthday() // returns 31
When the interpreter encounters p.greet(), it:
- Evaluates
pto get the receiver object. - Reads
p.__type__to get the type name ("Person"). - Looks up
"greet"inmethod_tables["Person"]. - Prepends
pto the argument list as theitparameter. - Calls the resolved function with the full argument list.
Static Methods
A method without it as its first parameter is a static method. Static methods are called on the type name itself, not on instances. See Static Methods for full details.
give Person {
define species() {
return "Homo sapiens"
}
}
Person.species() // "Homo sapiens"
Additive Blocks
Multiple give/impl blocks for the same type are additive. Each block adds its methods to the existing method table without removing previously defined methods.
thing Car {
make: String
}
give Car {
define brand(it) {
return it.make
}
}
give Car {
define honk(it) {
say "Beep!"
}
}
let c = Car { make: "Toyota" }
c.brand() // "Toyota"
c.honk() // prints "Beep!"
If a later block defines a method with the same name as an existing method, the later definition overwrites the earlier one in the method table.
Method Table Storage
The interpreter maintains two HashMap tables:
| Table | Key | Value | Lookup |
|---|---|---|---|
method_tables | Type name | IndexMap<String, Value> | Instance method dispatch |
static_methods | Type name | IndexMap<String, Value> | Static method dispatch |
When a give/impl block is executed, each method is inserted into method_tables under the type name. Methods without an it parameter are additionally inserted into static_methods.
Method Resolution Order
When resolving obj.method(args) on a typed object:
- Direct field — If the object has a field named
methodthat is callable, it is invoked. - Method table — The runtime looks up
method_tables[obj.__type__][method]. - Embedded delegation — If not found, the runtime checks each embedded field’s type for the method in
method_tables. See Composition. - Known builtins — Certain method names (e.g.,
map,filter,push) are recognized as builtin functions and dispatched accordingly. - Error — If no match is found, a runtime error is raised:
no method 'method' on TypeName.
Mixed Syntax
Natural (define) and classic (fn) function syntax may be used interchangeably within give and impl blocks:
give Greeter {
define hello(it) {
say "hi from " + it.name
}
fn goodbye(it) {
say "bye from " + it.name
}
}
Interface Contracts
Interfaces define behavioral contracts — a set of method signatures that a type must implement. Forge provides dual syntax: power (natural) and interface (classic). Implementing an interface is verified at definition time when the give...the power or impl...for block is executed.
Syntax
InterfaceDef = ("power" | "interface") Identifier "{" MethodSig* "}"
MethodSig = ("fn" | "define") Identifier "(" ParamList ")" ("->" TypeAnnotation)?
ImplInterface = "give" TypeName "the" "power" InterfaceName "{" MethodDef* "}"
| "impl" InterfaceName "for" TypeName "{" MethodDef* "}"
Defining an Interface
The power keyword (or its classic alias interface) introduces a named interface. An interface body contains method signatures — method names with parameter lists and optional return type annotations.
power Greetable {
fn greet(it) -> String
}
Classic syntax:
interface Greetable {
fn greet(it) -> String
}
Both forms produce the same InterfaceDef AST node.
Interface Registration
When an InterfaceDef statement is executed, the interpreter:
- Builds an array of method specification objects. Each object contains:
name— the method name as aString.param_count— the number of parameters as anInt.return_type— the return type annotation as aString, if present.
- Creates an interface metadata object with fields
__kind__: "interface",name, andmethods. - Registers the interface in the environment under both its name and
__interface_Name__.
The interface object is a regular Object value and can be passed to functions like satisfies.
Implementing an Interface
To declare that a type fulfills an interface, use give...the power (natural) or impl...for (classic):
thing Cat {
name: String
}
power Greetable {
fn greet(it) -> String
}
give Cat the power Greetable {
define greet(it) {
return "Meow, I'm " + it.name
}
}
Classic syntax:
interface Named {
fn get_name(it) -> String
}
impl Named for Animal {
fn get_name(it) {
return it.name
}
}
Validation at Definition Time
When a give...the power or impl...for block is executed, the runtime validates that every method required by the interface is present in the type’s method table (including methods added by the current block and any previous give/impl blocks).
The validation checks method presence by name. It does not verify parameter counts, parameter types, or return types.
If a required method is missing, a runtime error is raised:
'Cat' does not implement 'greet' required by power 'Greetable'
This error occurs at the point where the give...the power block is executed, not at a later call site.
Interface Without Explicit Implementation
A type may satisfy an interface without ever using give...the power or impl...for. Forge supports Go-style structural typing through the satisfies function. See Structural Satisfaction.
The give...the power syntax provides two benefits over implicit satisfaction:
- Early validation — Errors are reported at the implementation site rather than at a distant call site.
- Documentation — The code explicitly declares the relationship between a type and an interface.
Multiple Interfaces
A type may implement multiple interfaces through separate give...the power blocks:
power Speakable {
fn speak(it) -> String
}
power Trainable {
fn train(it, command: String)
fn obey(it, command: String) -> Bool
}
thing Dog {
name: String
}
give Dog the power Speakable {
define speak(it) {
return "Woof!"
}
}
give Dog the power Trainable {
define train(it, command) {
say it.name + " is learning " + command
}
define obey(it, command) {
return true
}
}
Each give...the power block independently validates that its interface’s requirements are met. Methods from earlier blocks (including plain give blocks without an interface) count toward satisfaction.
Return Type Annotations
Return type annotations in interface method signatures are stored in the interface metadata but are not enforced at runtime. They serve as documentation:
power Hashable {
fn hash(it) -> String
}
The -> String annotation is recorded in the method specification but the runtime does not check that hash actually returns a String.
Composition
Forge supports struct composition through the has keyword, which embeds one struct inside another. Embedded fields enable automatic delegation of both field access and method calls to the inner struct, providing a composition-based alternative to inheritance.
Syntax
EmbedField = "has" Identifier ":" TypeAnnotation
The has keyword appears in a struct field position and marks the field as embedded.
Defining Embedded Fields
Use has inside a struct definition to embed another type:
thing Address {
street: String,
city: String,
zip: String
}
thing Employee {
name: String,
has addr: Address
}
The addr field is an embedded field of type Address. The has keyword tells the runtime to register this field for delegation.
Construction
Embedded fields are initialized like regular fields during construction:
let emp = Employee {
name: "Alice",
addr: Address {
street: "123 Main St",
city: "Springfield",
zip: "62701"
}
}
Field Delegation
When a field is accessed on a struct instance and the field is not found directly on the object, the runtime checks each embedded sub-object for the field. This enables transparent access to inner fields:
// Direct access (always works)
emp.addr.city // "Springfield"
// Delegated access (through embedding)
emp.city // "Springfield"
emp.street // "123 Main St"
The delegation algorithm for obj.field:
- Check if
objhas a direct field namedfield. If found, return it. - Read
obj.__type__to get the type name. - Look up the type name in
embedded_fieldsto get the list of(field_name, type_name)pairs. - For each embedded field, check if
obj[field_name]is an object with the requestedfield. If found, return it. - If no embedded field contains the requested field, raise a runtime error.
Method Delegation
Method calls are also delegated to embedded types. If a method is not found in the outer type’s method table, the runtime searches each embedded type’s method table:
give Address {
define full(it) {
return it.street + ", " + it.city + " " + it.zip
}
}
// Called on the embedded Address through Employee
emp.full() // "123 Main St, Springfield 62701"
// Explicit path also works
emp.addr.full() // "123 Main St, Springfield 62701"
The method delegation algorithm for obj.method(args):
- Look up
methodinmethod_tables[obj.__type__]. If found, call it withobjprepended asit. - Look up
embedded_fields[obj.__type__]to get the list of embedded fields. - For each
(embed_field, embed_type)pair, look upmethodinmethod_tables[embed_type]. - If found, extract
obj[embed_field]as the receiver and call the method with the sub-object asit. - If no embedded type has the method, continue to builtin resolution or raise an error.
Multiple Embeddings
A struct may embed multiple fields:
thing Engine {
horsepower: Int
}
thing Chassis {
material: String
}
thing Car {
make: String,
has engine: Engine,
has chassis: Chassis
}
give Engine {
define rev(it) {
say "Vroom! " + str(it.horsepower) + "hp"
}
}
give Chassis {
define describe(it) {
return it.material + " chassis"
}
}
let c = Car {
make: "Toyota",
engine: Engine { horsepower: 200 },
chassis: Chassis { material: "Steel" }
}
c.rev() // prints "Vroom! 200hp"
c.describe() // "Steel chassis"
c.horsepower // 200
c.material // "Steel"
Embedded fields are searched in declaration order. If two embedded types provide the same field name, the first match wins.
Embedding and Interfaces
Delegated methods count toward interface satisfaction. If an embedded type’s method table contains a method required by an interface, the outer type satisfies that interface through delegation:
power Describable {
fn describe(it) -> String
}
// Car satisfies Describable through its embedded Chassis
satisfies(c, Describable) // true (via chassis.describe)
Storage
The interpreter maintains an embedded_fields table:
embedded_fields: HashMap<String, Vec<(String, String)>>
The key is the outer struct name. The value is a vector of (field_name, type_name) pairs, one for each has field in the struct definition. This table is populated when the StructDef statement is executed.
Structural Satisfaction
The satisfies function tests whether a value’s type fulfills an interface’s requirements at runtime, without requiring an explicit give...the power or impl...for declaration. This is Forge’s implementation of Go-style structural typing: if a type has the right methods, it satisfies the interface.
Syntax
satisfies(value, InterfaceObject) -> Bool
The satisfies function is a builtin that takes two arguments:
- value — Any value, typically a struct instance (an object with a
__type__field). - InterfaceObject — An interface value (an object with
__kind__: "interface"and amethodsarray).
It returns true if the value’s type has all methods required by the interface, false otherwise.
Basic Usage
thing Robot {
name: String
}
power Speakable {
fn speak(it) -> String
}
give Robot {
define speak(it) {
return "Beep boop, I am " + it.name
}
}
let r = Robot { name: "R2D2" }
satisfies(r, Speakable) // true
Note that Robot never explicitly declared give Robot the power Speakable. The satisfies check passes because Robot has a speak method in its method table.
Resolution Algorithm
The satisfies function checks interface satisfaction in two phases:
Phase 1: Structural Check
First, satisfies performs a structural check on the value itself. It examines whether the value (or the object’s fields) contains callable values matching each required method name. This handles objects that carry their methods as fields.
Phase 2: Method Table Check
If the structural check fails and the value is an object with a __type__ field, satisfies looks up the type name in the interpreter’s method_tables. For each method required by the interface, it checks whether the method table contains an entry with that name.
thing Printer {}
give Printer {
define print_line(it, text) {
say text
}
}
power Printable {
fn print_line(it, text: String)
}
let p = Printer {}
satisfies(p, Printable) // true (found in method_tables)
Satisfaction Criteria
The check verifies method presence by name only. It does not verify:
- Parameter count or parameter types
- Return types
- Method body or behavior
A type satisfies an interface if and only if every method name listed in the interface’s methods array has a corresponding entry in the type’s method table.
Explicit vs. Structural
Forge supports both explicit and structural interface satisfaction:
| Approach | Syntax | When Checked |
|---|---|---|
| Explicit | give T the power I { ... } | At definition time |
| Structural | satisfies(value, I) | At call time |
Explicit implementation triggers immediate validation and produces clear error messages at the definition site. Structural satisfaction is more flexible but defers errors to the point where satisfies is called.
Both approaches can coexist. A type that explicitly implements an interface will also pass satisfies checks.
Examples
Satisfied Without Explicit Declaration
thing Duck {
name: String
}
power Quackable {
fn quack(it) -> String
}
give Duck {
define quack(it) {
return it.name + " says quack!"
}
}
let d = Duck { name: "Donald" }
satisfies(d, Quackable) // true — Duck has quack()
Not Satisfied
thing Rock {
weight: Int
}
satisfies(Rock { weight: 5 }, Quackable) // false — Rock has no quack()
Multiple Interface Methods
power Serializable {
fn to_string(it) -> String
fn to_json(it) -> String
}
thing Config {
data: String
}
give Config {
define to_string(it) { return it.data }
// Missing to_json
}
let c = Config { data: "test" }
satisfies(c, Serializable) // false — missing to_json
Return Value
satisfies always returns a Bool value. It never throws an error for non-matching types; it simply returns false. It only raises a runtime error if called with the wrong number of arguments.
Default Values
Struct fields may specify default values that are applied when the field is omitted during construction. Default expressions are evaluated at definition time and stored in the interpreter’s struct_defaults table.
Syntax
FieldWithDefault = Identifier (":" TypeAnnotation)? "=" Expression
The default value follows an = sign after the field name and optional type annotation.
Defining Defaults
thing Config {
host: String = "localhost",
port: Int = 8080,
debug: Bool = false
}
A field may have a type annotation, a default, both, or neither:
| Field Form | Example |
|---|---|
| Name only | data |
| Name + type | data: String |
| Name + default | data = "hello" |
| Name + type + default | data: String = "hello" |
Evaluation Timing
Default expressions are evaluated at definition time — when the thing/struct statement is executed, not when an instance is constructed. This means:
let counter = 0
thing Widget {
id: Int = counter
}
change counter to 10
let w = Widget {}
say w.id // 0, not 10 — default was captured at definition time
The default value is the result of evaluating the expression at the point where the struct is defined. Subsequent changes to variables referenced in the default expression do not affect the stored default.
Application During Construction
When a struct instance is constructed, defaults are applied first, then explicitly provided fields override them:
- The interpreter retrieves the defaults from
struct_defaults[StructName]. - All default key-value pairs are inserted into the new object.
- Explicitly provided fields in the constructor are evaluated and inserted, overwriting any defaults with the same key.
- The
__type__field is inserted last.
thing Server {
host: String = "0.0.0.0",
port: Int = 3000,
workers: Int = 4
}
// All defaults
let s1 = Server {}
// s1 = { host: "0.0.0.0", port: 3000, workers: 4, __type__: "Server" }
// Partial override
let s2 = Server { port: 8080 }
// s2 = { host: "0.0.0.0", port: 8080, workers: 4, __type__: "Server" }
// Full override
let s3 = Server { host: "127.0.0.1", port: 443, workers: 16 }
// s3 = { host: "127.0.0.1", port: 443, workers: 16, __type__: "Server" }
Default Expressions
Default values may be any valid expression, not just literals. They are evaluated in the current scope at definition time:
let default_name = "World"
thing Greeter {
greeting: String = "Hello, " + default_name + "!"
}
let g = Greeter {}
say g.greeting // "Hello, World!"
Function calls, arithmetic, string concatenation, and other expressions are all valid defaults.
Storage
The interpreter stores defaults in a struct_defaults table:
struct_defaults: HashMap<String, IndexMap<String, Value>>
The outer key is the struct name. The inner IndexMap maps field names to their default Value. Only fields with defaults are stored; fields without defaults are absent from the map.
Fields Without Defaults
Fields without defaults must be provided during construction. If a field without a default is omitted, the constructed object simply will not have that field — no error is raised at construction time, but accessing the missing field later will produce a runtime error.
Static Methods
Static methods are methods attached to a type that do not operate on an instance. They are defined in give/impl blocks without it as the first parameter and are called on the type name itself.
Syntax
Static methods are defined like instance methods but without it:
give TypeName {
define method_name(params) {
// body — no `it` parameter
}
}
They are called on the type name:
TypeName.method_name(args)
Defining Static Methods
A method is classified as static when its first parameter is not named it. Any other parameter name (or no parameters at all) makes it a static method.
thing Person {
name: String,
age: Int
}
give Person {
// Static method — no `it` parameter
define species() {
return "Homo sapiens"
}
// Static method — first param is not `it`
define create(name, age) {
return Person { name: name, age: age }
}
// Instance method — first param IS `it`
define greet(it) {
say "Hello, I'm " + it.name
}
}
Invocation
Static methods are called using the type name with dot notation:
Person.species() // "Homo sapiens"
let p = Person.create("Bob", 25) // Person { name: "Bob", age: 25 }
The runtime resolves Person.method() by:
- Evaluating
Person— this yields theBuiltIn("struct:Person")sentinel value registered during struct definition. - Extracting the type name
"Person"from the sentinel tag. - Looking up the method name in
static_methods["Person"]. - Calling the function with the provided arguments (no instance prepended).
Storage
Static methods are stored in the static_methods table:
static_methods: HashMap<String, IndexMap<String, Value>>
The key is the type name. The value is an IndexMap mapping method names to function values.
When a give/impl block is executed, each method is checked for the it parameter:
| First Parameter | Stored In | Call Syntax |
|---|---|---|
it | method_tables only | instance.method() |
| Anything else | Both method_tables and static_methods | TypeName.method() |
Static methods are stored in both tables. This means they appear in method_tables as well, which allows them to be found during interface satisfaction checks.
Factory Pattern
A common use of static methods is the factory pattern — creating instances with validation or transformation logic:
thing Color {
r: Int,
g: Int,
b: Int
}
give Color {
define from_hex(hex) {
// Parse hex string to RGB values
return Color { r: 0, g: 0, b: 0 }
}
define red() {
return Color { r: 255, g: 0, b: 0 }
}
define display(it) {
return "rgb(" + str(it.r) + ", " + str(it.g) + ", " + str(it.b) + ")"
}
}
let c = Color.red()
say c.display() // "rgb(255, 0, 0)"
Additive Blocks
Like instance methods, static methods from multiple give/impl blocks are additive:
give Math {
define add(a, b) { return a + b }
}
give Math {
define sub(a, b) { return a - b }
}
Math.add(1, 2) // 3
Math.sub(5, 3) // 2
Instance vs. Static Ambiguity
The classification is based solely on whether the first parameter is named it. A method named it with a different first parameter name is static:
give Config {
// Static: first param is "key", not "it"
define from_key(key) {
return Config { value: key }
}
// Instance: first param is "it"
define to_string(it) {
return str(it.value)
}
}
Error Handling
This chapter defines Forge’s error handling mechanisms. Forge uses a multi-layered approach: Result types for explicit error values, the ? operator for propagation, safe blocks for error suppression, must for crash-on-error semantics, and check for declarative validation.
Overview
Forge provides five complementary error handling mechanisms:
| Mechanism | Purpose | Behavior on Error |
|---|---|---|
Result type | Represent success/failure as values | Carries error as data |
? operator | Propagate errors up the call stack | Returns Err from enclosing function |
safe { } | Suppress errors silently | Returns null |
must expr | Assert success or crash | Raises a runtime error |
check expr | Declarative validation | Raises a runtime error with description |
These mechanisms serve different use cases:
- Result + ? — For functions that can fail and callers that want to handle failures explicitly.
- safe — For optional operations where failure is acceptable and the value can be
null. - must — For operations that should never fail in correct code.
- check — For input validation with readable error messages.
Runtime Errors
All Forge runtime errors are represented by RuntimeError, which contains a message string and an optional propagated value. When an error is not caught, it terminates the program with an error message.
Errors can be caught with try/catch:
try {
let x = 1 / 0
} catch e {
say e.message // "division by zero"
say e.type // "ArithmeticError"
}
The catch variable receives an object with message and type fields. Error types are inferred from the error message content:
| Error Type | Triggered By |
|---|---|
TypeError | Message contains “type” or “Type” |
ArithmeticError | Message contains “division by zero” |
AssertionError | Message contains “assertion” |
IndexError | Message contains “index” or “out of bounds” |
ReferenceError | Message contains “not found” or “undefined” |
RuntimeError | All other errors |
Subsections
The following subsections define each error handling mechanism in detail:
- Result Type — Ok/Err values and inspection functions.
- Propagation — The
?operator. - Safe and Must — Error suppression and crash-on-error.
- Check — Declarative validation.
Result Type
The Result type represents the outcome of an operation that may succeed or fail. A Result is either Ok(value) for success or Err(message) for failure. Results are first-class values that can be stored in variables, passed to functions, and returned from functions.
Syntax
ResultOk = ("Ok" | "ok") "(" Expression ")"
ResultErr = ("Err" | "err") "(" Expression ")"
Constructors
Forge provides two constructors for creating Result values. Both accept case-insensitive names:
let success = Ok(42)
let failure = Err("something went wrong")
// Lowercase variants are equivalent
let success2 = ok(42)
let failure2 = err("not found")
Ok wraps any value:
Ok(42) // Result containing Int
Ok("hello") // Result containing String
Ok([1, 2, 3]) // Result containing Array
Ok(null) // Result containing null (Ok with no meaningful value)
Err wraps an error value (typically a string message):
Err("file not found")
Err("invalid input: expected number")
If Ok is called with no arguments, it wraps null. If Err is called with no arguments, it wraps the string "error".
Runtime Representation
Results are distinct variants in the Value enum:
Value::ResultOk(Box<Value>)— wraps the success value.Value::ResultErr(Box<Value>)— wraps the error value.
The typeof builtin returns "Result" for both variants:
typeof(Ok(1)) // "Result"
typeof(Err("x")) // "Result"
Display Format
Results are displayed as Ok(value) or Err(value):
say Ok(42) // Ok(42)
say Err("not found") // Err(not found)
In JSON serialization, Results produce { "Ok": value } or { "Err": value }.
Inspection Functions
Four builtin functions inspect and extract Result values:
is_ok(result)
Returns true if the value is Ok, false if Err. Raises a runtime error if the argument is not a Result.
is_ok(Ok(42)) // true
is_ok(Err("oops")) // false
is_err(result)
Returns true if the value is Err, false if Ok. Raises a runtime error if the argument is not a Result.
is_err(Ok(42)) // false
is_err(Err("oops")) // true
unwrap(result)
Extracts the inner value from Ok. If the value is Err, raises a runtime error with the message "unwrap() on Err: <error_value>".
unwrap(Ok(42)) // 42
unwrap(Err("oops")) // runtime error: unwrap() on Err: oops
unwrap also works with Option values (Some/None):
unwrap(Some(42)) // 42
unwrap(None) // runtime error: unwrap() called on None
unwrap_or(result, default)
Extracts the inner value from Ok. If the value is Err, returns the default value instead. Never raises an error for valid Result inputs.
unwrap_or(Ok(42), 0) // 42
unwrap_or(Err("oops"), 0) // 0
unwrap_or also works with Option values:
unwrap_or(Some(42), 0) // 42
unwrap_or(None, 0) // 0
Raises a runtime error if called with the wrong number of arguments or if the first argument is not a Result or Option.
Pattern Matching on Results
Results can be matched in match expressions using Ok and Err patterns:
let result = Ok(42)
match result {
Ok(value) -> say "Got: " + str(value),
Err(msg) -> say "Error: " + msg
}
Results in Functions
Functions commonly return Results to signal success or failure:
fn divide(a, b) {
if b == 0 {
return Err("division by zero")
}
return Ok(a / b)
}
let result = divide(10, 0)
if is_err(result) {
say "Cannot divide: " + unwrap(Err("division by zero"))
}
Equality
Two Ok values are equal if their inner values are equal. Two Err values are equal if their inner values are equal. Ok and Err are never equal to each other:
Ok(42) == Ok(42) // true
Err("x") == Err("x") // true
Ok(42) == Err(42) // false
Ok(1) == Ok(2) // false
Error Propagation
The ? operator provides concise syntax for propagating errors up the call stack. When applied to a Result value, it unwraps Ok values and short-circuits on Err values by returning the error from the enclosing function.
Syntax
TryExpr = Expression "?"
The ? operator is a postfix unary operator applied to an expression that evaluates to a Result.
Semantics
The ? operator evaluates its operand and inspects the result:
- If the value is
Ok(v), the expression evaluates tov(the inner value is unwrapped). - If the value is
Err(e), the enclosing function immediately returnsErr(e). - If the value is neither
OknorErr, a runtime error is raised:`?` expects Result value (Ok(...) or Err(...)).
fn parse_number(s) {
if s == "" {
return Err("empty string")
}
return Ok(int(s))
}
fn double_parsed(s) {
let n = parse_number(s)? // unwraps Ok or returns Err
return Ok(n * 2)
}
double_parsed("5") // Ok(10)
double_parsed("") // Err("empty string")
Propagation Mechanism
When ? encounters an Err value, it raises a RuntimeError with the propagated field set to the Err value. The runtime distinguishes propagated errors from ordinary runtime errors. When a propagated error reaches a function boundary, the function returns the propagated Err value rather than crashing the program.
The implementation:
Expr::Try(expr)evaluatesexpr.- If the result is
ResultOk(value), returnsvalue. - If the result is
ResultErr(err), callsRuntimeError::propagate(ResultErr(err)), which creates aRuntimeErrorwhosepropagatedfield carries the originalErrvalue. - The calling function’s error handler detects the propagated value and converts it back into a return value.
Chaining
The ? operator can be chained across multiple function calls:
fn read_config() {
let text = read_file("config.json")?
let parsed = json.parse(text)?
return Ok(parsed)
}
Each ? either unwraps the Ok value for the next step or short-circuits the entire function with the first Err encountered.
Requirements
The ? operator requires that its operand evaluates to a Result value. Applying ? to a non-Result value (such as an Int, String, or null) raises a runtime error:
let x = 42? // runtime error: `?` expects Result value (Ok(...) or Err(...))
Usage Patterns
Propagate and Transform
fn load_user(id) {
let data = fetch_user_data(id)?
let user = parse_user(data)?
return Ok(user)
}
Propagate with Fallback
Combine ? with unwrap_or for partial error handling:
fn get_config_or_default() {
let config = read_config()? // propagate file errors
let timeout = unwrap_or(config.timeout, 30) // fallback for missing field
return Ok(timeout)
}
Top-Level Handling
At the top level, propagated errors become runtime errors since there is no enclosing function to return from:
// If read_config returns Err, this crashes the program
let config = read_config()?
To handle errors at the top level, use is_ok/is_err checks, unwrap_or, or try/catch blocks instead of ?.
Safe and Must
safe and must provide two ends of the error handling spectrum: safe suppresses errors silently, while must crashes on error with a clear message.
Safe Blocks
Syntax
SafeBlock = "safe" "{" Statement* "}"
The safe keyword introduces a block whose errors are silently suppressed.
Semantics
A safe block executes its body statements. If any statement raises a runtime error, the error is caught and the block evaluates to null. If the block completes successfully, its result is returned normally.
safe {
let data = json.parse("invalid json")
say data
}
// No error — block silently returns null
Behavior
| Block Outcome | Result |
|---|---|
| Body completes successfully | Signal passes through (value, return, etc.) |
| Body raises a runtime error | null (error suppressed) |
The safe block is a statement, not an expression. It does not produce a value that can be assigned directly. When used for its side effects, it simply prevents errors from propagating:
// Attempt to write a file; ignore errors
safe {
fs.write("log.txt", "entry")
}
// Execution continues regardless
say "done"
Use Cases
- Optional side effects (logging, caching) where failure is acceptable.
- Defensive code around external operations (file I/O, network) that may fail intermittently.
- Quick prototyping where error handling is deferred.
Caution
safe blocks suppress all errors indiscriminately, including programming bugs, type errors, and logic errors. Overuse of safe can hide real problems. Prefer explicit error handling with Result/? for production code.
Must Expression
Syntax
MustExpr = "must" Expression
The must keyword is a prefix operator applied to an expression.
Semantics
must evaluates its operand and asserts that the result is a successful value:
- If the value is
Ok(v), returnsv(unwrapped). - If the value is
Err(e), raises a runtime error:"must failed: <e>". - If the value is
null, raises a runtime error:"must failed: got null". - For any other value, returns it unchanged.
// Succeeds — unwraps Ok
let value = must Ok(42) // 42
// Crashes — Err inside must
let value = must Err("oops") // runtime error: must failed: oops
// Crashes — null inside must
let value = must null // runtime error: must failed: got null
// Passes through — non-Result, non-null
let value = must 42 // 42
Comparison with unwrap
| Function | On Ok(v) | On Err(e) | On null | On other |
|---|---|---|---|---|
unwrap(r) | v | Error | Error (for None) | Error |
must expr | v | Error | Error | Pass through |
The key difference: must passes through non-Result, non-null values unchanged, while unwrap requires a Result or Option value. must is designed for contexts where the expression might return a Result, a plain value, or null.
Use Cases
-
Asserting that a critical operation succeeds:
let db = must db.open("app.db") -
Unwrapping configuration that should always be present:
let key = must env.get("API_KEY") -
Failing fast on unexpected null values:
let user = must find_user(id)
Combining Safe and Must
safe and must can be used together for fallback patterns:
// Try the primary source, fall back to default
safe {
let config = must load_config("primary.json")
apply_config(config)
}
// If must fails, safe catches it and continues
apply_config(default_config())
However, this pattern is generally better expressed with Result types:
let config = unwrap_or(load_config("primary.json"), default_config())
apply_config(config)
Check
The check statement provides declarative validation with clear error messages. It evaluates a condition and raises a runtime error if the check fails.
Syntax
CheckStmt = "check" Expression CheckKind
CheckKind = IsNotEmpty | Contains | Between | IsTrue
IsNotEmpty = "is" "not" "empty"
Contains = "contains" Expression
Between = "is" "between" Expression "and" Expression
IsTrue = (empty — implicit truth check)
Check Kinds
Forge supports four validation kinds, each producing a specific boolean test:
is not empty
Tests that a value is non-empty. The definition of “empty” depends on the type:
| Type | Empty When |
|---|---|
String | Length is 0 |
Array | Length is 0 |
Null | Always empty |
| Other types | Never empty |
let name = "Alice"
check name is not empty // passes
let empty = ""
check empty is not empty // runtime error: check failed: "" did not pass validation
contains
Tests that a string contains a substring:
let email = "user@example.com"
check email contains "@" // passes
let bad = "not-an-email"
check bad contains "@" // runtime error: check failed
The contains check currently operates on strings only. Both the value and the needle must be strings; other type combinations return false.
is between … and
Tests that a numeric value falls within an inclusive range:
let age = 25
check age is between 0 and 150 // passes
let temp = -10
check temp is between 0 and 100 // runtime error: check failed
Both Int and Float values are supported, but the value and both bounds must be the same type. Mixed-type comparisons (e.g., Int value with Float bounds) return false.
Implicit Truth Check
When no check kind is specified, the value is tested for truthiness:
let valid = true
check valid // passes
let invalid = false
check invalid // runtime error: check failed
Error Messages
When a check fails, the runtime raises a RuntimeError with the message:
check failed: <value> did not pass validation
Where <value> is the string representation of the tested value.
Use Cases
Input Validation
fn create_user(name, age, email) {
check name is not empty
check age is between 0 and 150
check email contains "@"
return { name: name, age: age, email: email }
}
Preconditions
fn withdraw(account, amount) {
check amount is between 1 and 10000
check account.balance is between amount and 999999
account.balance = account.balance - amount
}
Configuration Validation
let config = load_config()
check config.host is not empty
check config.port is between 1 and 65535
Comparison with Assert
| Feature | check | assert |
|---|---|---|
| Purpose | Declarative validation | General assertion |
| Syntax | check expr is not empty | assert(condition, "message") |
| Error message | Auto-generated from value | User-provided |
| Kinds | is not empty, contains, is between, truth | Boolean only |
check is designed for readable validation logic with automatic error descriptions. assert is a general-purpose assertion that requires the programmer to provide an error message.
Nesting in Safe Blocks
Check failures are runtime errors and can be caught by safe blocks or try/catch:
safe {
check "" is not empty // fails, but error is suppressed
}
say "continues" // prints "continues"
try {
check input is not empty
} catch e {
say "Validation error: " + e.message
}
Concurrency
This chapter defines Forge’s concurrency primitives. Forge provides three mechanisms for concurrent execution: channels for message passing, spawn for task creation, and async/await (with natural syntax aliases forge/hold) for asynchronous functions.
Overview
Forge’s concurrency model is built on OS threads (via std::thread) with channels for communication:
| Mechanism | Purpose | Syntax |
|---|---|---|
| Channels | Message passing between tasks | channel(), send(), receive() |
| Spawn | Create concurrent tasks | spawn { body } |
| Async/Await | Asynchronous function definition and invocation | async fn / await, forge / hold |
Execution Model
Forge spawns concurrent tasks as OS threads. Each spawned task receives a clone of the current environment, enabling access to variables defined before the spawn point. Tasks do not share mutable state directly; communication should use channels.
The runtime uses std::thread::spawn for task creation and std::sync::mpsc::sync_channel for channels. This provides:
- True parallelism on multi-core systems.
- Thread-safe communication through bounded channels.
- Task handle values with condition-variable notification for
await.
Task Handles
When spawn is used as an expression, it returns a TaskHandle value. Task handles are opaque values that can be passed to await (or hold) to block until the spawned task completes and retrieve its return value.
let handle = spawn { return 42 }
let result = await handle // 42
Task handles use an Arc<(Mutex<Option<Value>>, Condvar)> internally:
- The
Mutex<Option<Value>>holds the task’s return value (initiallyNone). - The
Condvaris notified when the task writes its result. awaitblocks on theCondvaruntil the result is available, then extracts it.
Error Isolation
Errors in spawned tasks do not crash the parent task. If a spawned task encounters a runtime error, the error is printed to stderr and the task’s result is null:
spawn { let x = 1 / 0 } // prints error to stderr, does not crash parent
say "still running" // executes normally
Subsections
The following subsections define each concurrency mechanism in detail:
- Channels — Message passing between tasks.
- Spawn — Creating concurrent tasks.
- Async Functions — Defining asynchronous functions.
- Await — Waiting for async results.
Channels
Channels provide thread-safe message passing between concurrent tasks. A channel is a bounded, synchronous queue that allows one task to send values and another to receive them.
Creating a Channel
Syntax
channel()
channel(capacity)
The channel builtin creates a new channel and returns a Channel value. An optional integer argument specifies the buffer capacity (default: 32).
let ch = channel() // buffered channel, capacity 32
let ch = channel(100) // buffered channel, capacity 100
let ch = channel(1) // minimal buffer, capacity 1
If a non-integer argument is provided, the default capacity of 32 is used. The minimum capacity is 1 (values less than 1 are clamped to 1).
Channel Value
A channel is represented at runtime as Value::Channel(Arc<ChannelInner>). The ChannelInner struct contains:
tx: Mutex<Option<SyncSender<Value>>>— the sender half.rx: Mutex<Option<Receiver<Value>>>— the receiver half.capacity: usize— the buffer capacity.
Channels are reference-counted via Arc, so they can be safely shared between the parent task and spawned tasks through environment cloning.
Sending Values
send(channel, value)
The send builtin sends a value through a channel. It blocks if the channel buffer is full, waiting until a receiver consumes a value.
let ch = channel()
send(ch, 42)
send(ch, "hello")
send(ch, [1, 2, 3])
Any Forge value can be sent through a channel: integers, strings, arrays, objects, functions, Results, and other channels.
Arguments:
channel— AChannelvalue (first argument).value— AnyValueto send (second argument).
Returns: null on success.
Errors:
"send(channel, value) requires 2 arguments"— Wrong argument count."send() requires a channel as first argument"— First argument is not a channel."channel closed"— The receiver has been dropped.
try_send(channel, value)
The try_send builtin attempts to send a value without blocking. Returns true if the value was sent, false if the channel is full or closed.
let ch = channel(1)
send(ch, "first") // fills the buffer
let ok = try_send(ch, "second") // false — buffer is full
Arguments: Same as send.
Returns: Bool — true if sent, false otherwise.
Errors:
"try_send() requires (channel, value)"— Wrong argument count."try_send() first argument must be a channel"— First argument is not a channel.
Receiving Values
receive(channel)
The receive builtin receives a value from a channel. It blocks until a value is available.
let ch = channel()
send(ch, 42)
let val = receive(ch) // 42
If the channel is closed (all senders dropped) and the buffer is empty, receive returns null.
Arguments:
channel— AChannelvalue.
Returns: The received Value, or null if the channel is closed.
Errors:
"receive(channel) requires 1 argument"— No argument provided."receive() requires a channel as first argument"— Argument is not a channel.
try_receive(channel)
The try_receive builtin attempts to receive a value without blocking. Returns Some(value) if a value was available, None if the channel is empty.
let ch = channel()
let result = try_receive(ch) // None — nothing sent yet
send(ch, 42)
let result = try_receive(ch) // Some(42)
Arguments:
channel— AChannelvalue.
Returns: Some(value) if a value was received, None if the channel is empty or closed.
Errors:
"try_receive() requires a channel"— No argument provided."try_receive() argument must be a channel"— Argument is not a channel.
Producer-Consumer Pattern
Channels enable classic producer-consumer patterns:
let ch = channel()
// Producer
spawn {
repeat 5 times {
send(ch, it)
}
}
// Consumer
repeat 5 times {
let val = receive(ch)
say "got: " + str(val)
}
Fan-Out Pattern
Multiple consumers can share a channel, though only one will receive each message:
let work = channel()
let results = channel()
// Producer
spawn {
for item in tasks {
send(work, item)
}
}
// Workers
repeat 3 times {
spawn {
let item = receive(work)
let result = process(item)
send(results, result)
}
}
Channel Lifetime
Channels remain open as long as at least one reference exists. When all references to a channel are dropped (through garbage collection or scope exit), the underlying SyncSender and Receiver are dropped, which closes the channel. Subsequent send calls on a closed channel return an error; receive calls return null.
Spawn
The spawn keyword creates a concurrent task that runs in a separate OS thread. It can be used as a statement (fire-and-forget) or as an expression (returning a task handle).
Syntax
SpawnStmt = "spawn" Block
SpawnExpr = "spawn" Block
When spawn appears as a statement, the result is discarded. When it appears as an expression (e.g., assigned to a variable), it returns a TaskHandle.
Statement Form (Fire-and-Forget)
When spawn is used as a statement, the block is executed concurrently and its result is discarded:
spawn {
say "running in background"
}
say "continues immediately"
The parent does not wait for the spawned task to complete. The spawned block runs independently.
Expression Form (Task Handle)
When spawn is used as an expression, it returns a TaskHandle that can be awaited:
let handle = spawn {
return 42
}
let result = await handle // 42
The task handle is an opaque value that represents the running task. Its type name is "TaskHandle".
Execution Model
When spawn is executed:
- The block’s statements are cloned.
- A new
Interpreteris created and its environment is cloned from the parent. - A shared result slot is created:
Arc<(Mutex<Option<Value>>, Condvar)>. - A new OS thread is spawned via
std::thread::spawn. - The thread executes the block. When it completes:
Signal::Return(v)orSignal::ImplicitReturn(v)storesvin the result slot.Signal::Noneor other signals storenull.- Errors print to stderr and store
null.
- The
Condvaris notified, unblocking anyawaiton the handle. - The
TaskHandlevalue is returned to the parent.
Environment Cloning
The spawned task receives a clone of the parent’s environment at the point of the spawn call. This means:
- Variables defined before
spawnare accessible in the spawned block. - Modifications to the environment inside the spawned block do not affect the parent.
- Modifications in the parent after
spawndo not affect the spawned block.
let x = 10
spawn {
say x // 10 — sees parent's x
let x = 20 // shadows, does not affect parent
}
say x // 10 — parent's x unchanged
Return Values
The spawned block may use return to provide a result. This value is stored in the task handle’s result slot:
let h = spawn {
return "hello from spawn"
}
let msg = await h // "hello from spawn"
If no return is used, the result is the last expression value (implicit return) or null:
let h = spawn {
1 + 1
}
let result = await h // may be 2 or null depending on block signal
Error Isolation
Errors in spawned tasks are isolated from the parent. A runtime error in the spawned block:
- Prints the error message to stderr:
spawn error: <message>. - Stores
nullin the result slot. - Does not crash or affect the parent task.
spawn {
let x = 1 / 0 // error: division by zero
}
// Parent continues normally
say "still running"
When the handle is awaited, the result is null:
let h = spawn {
return 1 / 0
}
let result = await h // null (error was caught internally)
Multiple Spawns
Multiple tasks can be spawned and awaited:
let a = spawn { return 10 }
let b = spawn { return 20 }
let va = await a // 10
let vb = await b // 20
say va + vb // 30
Tasks run concurrently in separate threads. The order of completion is non-deterministic.
Spawn with Channels
Spawn and channels work together for structured concurrency:
let ch = channel()
spawn {
let result = expensive_computation()
send(ch, result)
}
// Do other work...
let result = receive(ch) // blocks until computation completes
Async Functions
Async functions are functions that execute asynchronously and must be awaited to retrieve their result. Forge provides dual syntax: async fn (classic) and forge (natural).
Syntax
AsyncFnDef = ("async" "fn" | "forge") Identifier "(" ParamList ")" Block
Defining Async Functions
Classic syntax:
async fn fetch_data() {
let resp = http.get("https://api.example.com/data")
return resp
}
Natural syntax:
forge fetch_data() {
let resp = http.get("https://api.example.com/data")
return resp
}
Both forms are semantically identical. The parser produces the same FnDef AST node with is_async: true.
Semantics
An async function is defined with the is_async flag set to true in the AST. In the current implementation, async functions are stored as regular function values. The is_async flag is recorded in the AST but the interpreter treats async functions identically to synchronous functions during definition.
The distinction becomes relevant at the call site: async functions are expected to be called with await (or hold) to retrieve their result. Without await, the function executes synchronously and its return value is available immediately.
Registration
When an async function definition is executed, the function is registered in the environment as a Value::Function just like a synchronous function. The is_async flag from the AST does not alter the stored function value.
forge get_value() {
return 42
}
// The function is callable like any other function
let result = get_value() // 42 (runs synchronously)
let result = await get_value() // 42 (await passes through non-handle values)
Parameters and Return Values
Async functions support the same parameter syntax as regular functions, including default values and type annotations:
async fn fetch_user(id: Int, timeout: Int = 30) {
let resp = http.get("https://api.example.com/users/" + str(id))
return json.parse(resp.body)
}
Return values follow the same rules as synchronous functions. The return statement provides an explicit return value; without it, the function returns null or the last expression’s value.
Combining with Spawn
For true concurrent execution, combine async functions with spawn:
forge compute(n) {
// Expensive computation
return n * n
}
let handle = spawn { return compute(42) }
let result = await handle // 1764
The spawn keyword is what creates actual concurrency (a new OS thread). The async/forge keyword marks intent but does not itself create a new thread.
Natural Syntax: forge
The forge keyword serves double duty as both the language name and the natural-syntax alias for async fn. In a function definition context, forge is parsed as an async function definition:
forge load_config() {
let text = fs.read("config.json")
return json.parse(text)
}
This reads naturally as “forge a load_config function” while being functionally equivalent to async fn load_config().
Await
The await keyword (or its natural alias hold) suspends the current execution until an asynchronous operation completes, then returns the result. It is primarily used with task handles from spawn.
Syntax
AwaitExpr = ("await" | "hold") Expression
Semantics
The await expression evaluates its operand and inspects the result:
TaskHandle
If the operand is a TaskHandle (returned by spawn), await blocks the current thread until the spawned task completes, then returns the task’s result value.
let h = spawn { return 42 }
let result = await h // blocks until task completes, returns 42
The blocking mechanism uses a condition variable:
- Lock the
Mutex<Option<Value>>inside the task handle. - While the value is
None, wait on theCondvar. - When notified (the spawned task stored its result), extract the value.
- Return the extracted value, or
nullif the slot was empty.
Non-Handle Values (Pass-Through)
If the operand is not a TaskHandle, await returns the value unchanged. This provides backward compatibility and allows await to be used uniformly:
await 42 // 42
await "hello" // "hello"
await null // null
await Ok(10) // Ok(10)
This pass-through behavior means await is always safe to call, even on values that are not async results.
Natural Syntax: hold
The hold keyword is the natural-syntax alias for await:
let h = spawn { return "data" }
let result = hold h // "data"
hold and await are parsed to the same Expr::Await AST node and behave identically.
Awaiting Multiple Tasks
Multiple task handles can be awaited sequentially:
let a = spawn { return 10 }
let b = spawn { return 20 }
let c = spawn { return 30 }
let va = await a // 10
let vb = await b // 20
let vc = await c // 30
say va + vb + vc // 60
Each await blocks until its specific task completes. Tasks run concurrently, so the total time is approximately the duration of the slowest task, not the sum.
Awaiting Errored Tasks
If a spawned task encountered a runtime error, its result slot contains null. Awaiting such a handle returns null:
let h = spawn {
return 1 / 0 // runtime error
}
let result = await h // null
The error is printed to stderr by the spawned task. The parent receives null and must check for it if error detection is needed.
Await in Functions
await can be used inside regular and async functions:
fn parallel_sum(a, b) {
let ha = spawn { return a * a }
let hb = spawn { return b * b }
return await ha + await hb
}
parallel_sum(3, 4) // 25
Error Handling
The await expression can raise runtime errors in two cases:
"await: task handle lock poisoned"— TheMutexguarding the result slot was poisoned (the spawned thread panicked while holding the lock)."await: condvar wait failed"— The condition variable wait failed.
Both are exceptional conditions that indicate a serious runtime problem.
Comparison with hold
| Syntax | Keyword | Parsing | Behavior |
|---|---|---|---|
| Classic | await expr | Expr::Await(expr) | Block until result |
| Natural | hold expr | Expr::Await(expr) | Block until result |
There is no semantic difference. Use whichever style matches your code’s convention.
Standard Library Overview
Forge ships with 16 built-in modules containing over 230 functions. All modules are available without any import statement. There is no import math or require("fs") – every module is pre-loaded into the global scope.
Accessing Modules
Modules are accessed via dot notation:
let root = math.sqrt(144) // 12.0
let data = fs.read("config.json")
let hash = crypto.sha256("hello")
Each module is a first-class object. You can assign it to a variable:
let m = math
say m.pi // 3.141592653589793
Module Index
| Module | Description | Functions |
|---|---|---|
math | Mathematical operations and constants | 17 |
fs | File system operations | 20 |
io | Input/output and command-line arguments | 6 |
crypto | Hashing, encoding, and decoding | 6 |
db | SQLite database operations | 4 |
pg | PostgreSQL database operations | 4 |
json | JSON parsing and serialization | 3 |
csv | CSV parsing and serialization | 4 |
regex | Regular expression matching | 5 |
env | Environment variables | 4 |
log | Structured logging with timestamps | 4 |
term | Terminal colors, formatting, and widgets | 25+ |
http | HTTP client and server decorators | 9 |
exec | External command execution | 1 |
time | Date, time, and timezone operations | 25 |
npc | Fake data generation for testing | 16 |
Execution Tier Support
All modules are fully supported in the interpreter (default execution mode). The bytecode VM (--vm) and JIT (--jit) support a subset of modules – primarily math, fs, io, and npc. For full module access, use the interpreter.
math
Mathematical operations and constants. All trigonometric functions use radians.
Constants
| Name | Type | Value |
|---|---|---|
math.pi | float | 3.141592653589793 |
math.e | float | 2.718281828459045 |
math.inf | float | Infinity |
say math.pi // 3.141592653589793
say math.e // 2.718281828459045
Functions
math.sqrt(n) -> float
Returns the square root of n.
math.sqrt(144) // 12.0
math.sqrt(2) // 1.4142135623730951
math.pow(base, exp) -> int | float
Returns base raised to the power of exp. Returns int when both arguments are non-negative integers; returns float otherwise.
math.pow(2, 10) // 1024
math.pow(2.0, 0.5) // 1.4142135623730951
math.pow(2, -1) // 0.5
math.abs(n) -> int | float
Returns the absolute value of n. Preserves the input type.
math.abs(-42) // 42
math.abs(-3.14) // 3.14
math.max(a, b) -> int | float
Returns the greater of a and b.
math.max(10, 20) // 20
math.max(3.14, 2.71) // 3.14
math.min(a, b) -> int | float
Returns the lesser of a and b.
math.min(10, 20) // 10
math.min(3.14, 2.71) // 2.71
math.floor(n) -> int
Returns the largest integer less than or equal to n.
math.floor(3.7) // 3
math.floor(-1.2) // -2
math.floor(5) // 5
math.ceil(n) -> int
Returns the smallest integer greater than or equal to n.
math.ceil(3.2) // 4
math.ceil(-1.8) // -1
math.ceil(5) // 5
math.round(n) -> int
Returns the nearest integer to n, rounding half away from zero.
math.round(3.5) // 4
math.round(3.4) // 3
math.round(-2.5) // -3
math.random() -> float
Returns a pseudo-random float between 0.0 and 1.0 (exclusive).
let r = math.random() // e.g. 0.482371...
math.random_int(min, max) -> int
Returns a pseudo-random integer in the inclusive range [min, max]. Errors if min > max.
let die = math.random_int(1, 6) // 1-6
let coin = math.random_int(0, 1) // 0 or 1
math.sin(n) -> float
Returns the sine of n (in radians).
math.sin(0) // 0.0
math.sin(math.pi/2) // 1.0
math.cos(n) -> float
Returns the cosine of n (in radians).
math.cos(0) // 1.0
math.cos(math.pi) // -1.0
math.tan(n) -> float
Returns the tangent of n (in radians).
math.tan(0) // 0.0
math.tan(math.pi/4) // ~1.0
math.log(n) -> float
Returns the natural logarithm (base e) of n.
math.log(1) // 0.0
math.log(math.e) // 1.0
math.clamp(value, min, max) -> int | float
Clamps value to the range [min, max].
math.clamp(5, 1, 10) // 5
math.clamp(-5, 0, 10) // 0
math.clamp(15, 0, 10) // 10
fs
File system operations. All paths are strings. Functions that write to the file system return null on success.
Functions
fs.read(path) -> string
Reads the entire file at path and returns its contents as a string.
let content = fs.read("config.txt")
say content
fs.write(path, content) -> null
Writes content to the file at path, creating or overwriting the file.
fs.write("output.txt", "Hello, world!")
fs.append(path, content) -> null
Appends content to the file at path. Creates the file if it does not exist.
fs.append("log.txt", "New log entry\n")
fs.exists(path) -> bool
Returns true if a file or directory exists at path.
if fs.exists("config.json") {
say "Config found"
}
fs.list(path) -> array
Returns an array of file and directory names in the directory at path. Names only, not full paths.
let files = fs.list("./src")
// ["main.fg", "utils.fg", "lib"]
fs.remove(path) -> null
Deletes a file or directory (recursively) at path.
fs.remove("temp.txt")
fs.remove("build/") // removes directory and all contents
fs.mkdir(path) -> null
Creates the directory at path, including any necessary parent directories.
fs.mkdir("build/output/logs")
fs.copy(source, destination) -> int
Copies a file from source to destination. Returns the number of bytes copied.
let bytes = fs.copy("original.txt", "backup.txt")
say bytes // e.g. 1024
fs.rename(old_path, new_path) -> null
Renames or moves a file or directory.
fs.rename("draft.txt", "final.txt")
fs.size(path) -> int
Returns the size of the file at path in bytes.
let s = fs.size("data.bin")
say s // e.g. 4096
fs.ext(path) -> string
Returns the file extension without the leading dot. Returns an empty string if none.
fs.ext("photo.png") // "png"
fs.ext("Makefile") // ""
fs.read_json(path) -> any
Reads a JSON file and returns the parsed Forge value (object, array, etc.).
let config = fs.read_json("config.json")
say config.name
fs.write_json(path, value) -> null
Serializes value as pretty-printed JSON and writes it to path.
let data = { name: "forge", version: "0.3.3" }
fs.write_json("package.json", data)
fs.lines(path) -> array
Reads a file and returns an array of strings, one per line.
let lines = fs.lines("data.csv")
say len(lines) // number of lines
fs.dirname(path) -> string
Returns the directory portion of path.
fs.dirname("/home/user/file.txt") // "/home/user"
fs.basename(path) -> string
Returns the file name portion of path.
fs.basename("/home/user/file.txt") // "file.txt"
fs.join_path(a, b) -> string
Joins two path segments with the platform path separator.
fs.join_path("/home", "user") // "/home/user"
fs.is_dir(path) -> bool
Returns true if path is a directory.
fs.is_dir("/tmp") // true
fs.is_dir("file.txt") // false
fs.is_file(path) -> bool
Returns true if path is a regular file.
fs.is_file("main.fg") // true
fs.is_file("/tmp") // false
fs.temp_dir() -> string
Returns the path to the system temporary directory.
let tmp = fs.temp_dir()
say tmp // e.g. "/tmp"
crypto
Hashing and encoding utilities. All functions accept and return strings.
Functions
crypto.sha256(input) -> string
Returns the SHA-256 hash of input as a lowercase hex string.
crypto.sha256("hello")
// "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
crypto.md5(input) -> string
Returns the MD5 hash of input as a lowercase hex string.
crypto.md5("hello")
// "5d41402abc4b2a76b9719d911017c592"
Note: MD5 is cryptographically broken. Use
crypto.sha256for security-sensitive hashing. MD5 is provided for compatibility and checksums only.
crypto.base64_encode(input) -> string
Encodes input as a Base64 string using the standard alphabet.
crypto.base64_encode("hello world")
// "aGVsbG8gd29ybGQ="
crypto.base64_decode(input) -> string
Decodes a Base64 string back to its original form.
crypto.base64_decode("aGVsbG8gd29ybGQ=")
// "hello world"
crypto.hex_encode(input) -> string
Encodes input as a hexadecimal string.
crypto.hex_encode("AB")
// "4142"
crypto.hex_decode(input) -> string
Decodes a hexadecimal string back to its original form.
crypto.hex_decode("4142")
// "AB"
db
SQLite database operations. Forge embeds SQLite via the rusqlite crate. One connection is maintained per thread.
Functions
db.open(path) -> bool
Opens a SQLite database at path. Use ":memory:" for an in-memory database. Returns true on success.
db.open(":memory:")
db.open("app.db")
db.execute(sql, params?) -> null
Executes a SQL statement that does not return rows (CREATE, INSERT, UPDATE, DELETE). An optional second argument provides parameterized values as an array, using ? placeholders.
db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
// Parameterized insert (recommended)
db.execute("INSERT INTO users VALUES (?, ?, ?)", [1, "Alice", "alice@example.com"])
// Batch execution (no params)
db.execute("INSERT INTO users VALUES (2, 'Bob', 'bob@example.com')")
db.query(sql, params?) -> array
Executes a SQL SELECT query and returns an array of objects. Each object maps column names to values. An optional second argument provides parameterized values.
let users = db.query("SELECT * FROM users")
// [{id: 1, name: "Alice", email: "alice@example.com"}, ...]
// Parameterized query
let result = db.query("SELECT * FROM users WHERE id = ?", [1])
say result[0].name // "Alice"
db.close() -> null
Closes the current database connection.
db.close()
Full CRUD Example
// Open database
db.open(":memory:")
// Create table
db.execute("CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
done INTEGER DEFAULT 0
)")
// Create
db.execute("INSERT INTO tasks (title) VALUES (?)", ["Buy groceries"])
db.execute("INSERT INTO tasks (title) VALUES (?)", ["Write documentation"])
db.execute("INSERT INTO tasks (title) VALUES (?)", ["Deploy v2"])
// Read
let all_tasks = db.query("SELECT * FROM tasks")
say all_tasks
let pending = db.query("SELECT * FROM tasks WHERE done = ?", [0])
say len(pending) // 3
// Update
db.execute("UPDATE tasks SET done = ? WHERE id = ?", [1, 1])
// Delete
db.execute("DELETE FROM tasks WHERE id = ?", [3])
// Verify
let remaining = db.query("SELECT * FROM tasks")
say remaining
// Close
db.close()
Notes
- SQLite types map to Forge types: INTEGER ->
int, REAL ->float, TEXT ->string, NULL ->null, BLOB ->string(as<blob N bytes>). - Parameterized queries (using
?placeholders with an array) are the recommended approach to prevent SQL injection.
pg
PostgreSQL database operations. Requires a running PostgreSQL server. Uses tokio-postgres under the hood and requires the async runtime (interpreter mode).
Functions
pg.connect(connection_string) -> bool
Connects to a PostgreSQL database. Returns true on success. The connection string follows the standard PostgreSQL format.
pg.connect("host=localhost user=postgres password=secret dbname=myapp")
pg.query(sql) -> array
Executes a SQL SELECT query and returns an array of objects. Each object maps column names to values.
let users = pg.query("SELECT id, name, email FROM users")
for user in users {
say user.name
}
pg.execute(sql) -> int
Executes a SQL statement that does not return rows. Returns the number of rows affected.
let count = pg.execute("UPDATE users SET active = true WHERE last_login > now() - interval '30 days'")
say count // number of rows updated
pg.close() -> null
Closes the current PostgreSQL connection.
pg.close()
Example
pg.connect("host=localhost dbname=shop user=postgres")
pg.execute("CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2)
)")
pg.execute("INSERT INTO products (name, price) VALUES ('Widget', 9.99)")
pg.execute("INSERT INTO products (name, price) VALUES ('Gadget', 24.99)")
let products = pg.query("SELECT * FROM products ORDER BY price")
for p in products {
say p.name + " - $" + str(p.price)
}
pg.close()
Notes
- PostgreSQL types map to Forge types:
integer/bigint->int,real/double precision/numeric->float,text/varchar->string,boolean->bool, NULL ->null. - The
pgmodule requires the interpreter (default) execution mode. It is not available in the VM or JIT tiers.
json
JSON parsing and serialization.
Functions
json.parse(string) -> any
Parses a JSON string and returns the corresponding Forge value. Objects become Forge objects, arrays become Forge arrays, and JSON primitives map to their Forge equivalents.
let data = json.parse('{"name": "Forge", "version": 3}')
say data.name // "Forge"
say data.version // 3
let arr = json.parse("[1, 2, 3]")
say arr[0] // 1
json.stringify(value) -> string
Serializes a Forge value into a compact JSON string (no extra whitespace).
let obj = { name: "Forge", tags: ["fast", "fun"] }
let s = json.stringify(obj)
say s // {"name": "Forge", "tags": ["fast", "fun"]}
json.pretty(value, indent?) -> string
Serializes a Forge value into a pretty-printed JSON string. The optional indent parameter specifies the number of spaces per indentation level (default: 2).
let obj = { name: "Forge", version: 3 }
say json.pretty(obj)
// {
// "name": "Forge",
// "version": 3
// }
say json.pretty(obj, 4)
// {
// "name": "Forge",
// "version": 3
// }
Type Mapping
| JSON | Forge |
|---|---|
null | null |
true / false | bool |
| integer number | int |
| floating-point number | float |
"string" | string |
[...] | array |
{...} | object |
csv
CSV parsing and serialization. Uses comma-separated values with automatic type inference for fields.
Functions
csv.parse(string) -> array
Parses a CSV string into an array of objects. The first line is treated as headers. Values are automatically converted to int, float, or bool where possible; otherwise they remain strings.
let data = csv.parse("name,age,active\nAlice,30,true\nBob,25,false")
say data[0].name // "Alice"
say data[0].age // 30
say data[1].active // false
csv.stringify(rows) -> string
Converts an array of objects into a CSV string. Headers are derived from the keys of the first object.
let rows = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 }
]
let output = csv.stringify(rows)
say output
// name,age
// Alice,30
// Bob,25
Values containing commas or quotes are automatically quoted.
csv.read(path) -> array
Reads a CSV file from disk and parses it into an array of objects. Equivalent to csv.parse(fs.read(path)).
let users = csv.read("users.csv")
for user in users {
say user.name + ": " + str(user.email)
}
csv.write(path, rows) -> null
Serializes an array of objects as CSV and writes it to the file at path.
let data = [
{ product: "Widget", price: 9.99, qty: 100 },
{ product: "Gadget", price: 24.99, qty: 50 }
]
csv.write("inventory.csv", data)
regex
Regular expression operations. Uses Rust’s regex crate syntax.
Important: All
regexfunctions take the text first, pattern second:regex.test(text, pattern). This is the opposite of many other languages.
Functions
regex.test(text, pattern) -> bool
Returns true if pattern matches anywhere in text.
regex.test("hello world", "world") // true
regex.test("hello world", "^world") // false
regex.test("abc123", "\\d+") // true
regex.find(text, pattern) -> string | null
Returns the first match of pattern in text, or null if no match.
regex.find("order-4521-confirmed", "\\d+") // "4521"
regex.find("no numbers here", "\\d+") // null
regex.find_all(text, pattern) -> array
Returns an array of all non-overlapping matches of pattern in text.
regex.find_all("call 555-1234 or 555-5678", "\\d{3}-\\d{4}")
// ["555-1234", "555-5678"]
regex.find_all("aabbaab", "a+")
// ["aa", "aa"]
regex.replace(text, pattern, replacement) -> string
Replaces all occurrences of pattern in text with replacement.
regex.replace("hello world", "world", "Forge")
// "hello Forge"
regex.replace("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})", "$2/$3/$1")
// "01/15/2024"
The replacement string supports capture group references ($1, $2, etc.).
regex.split(text, pattern) -> array
Splits text by occurrences of pattern and returns an array of substrings.
regex.split("one,,two,,,three", ",+")
// ["one", "two", "three"]
regex.split("hello world foo", "\\s+")
// ["hello", "world", "foo"]
env
Environment variable access.
Functions
env.get(key, default?) -> string | null
Returns the value of the environment variable key. Returns null if the variable is not set, or default if provided.
let home = env.get("HOME")
say home // "/Users/alice"
let port = env.get("PORT", "8080")
say port // "8080" if PORT is not set
env.set(key, value) -> null
Sets the environment variable key to value for the current process.
env.set("APP_MODE", "production")
env.has(key) -> bool
Returns true if the environment variable key is set.
if env.has("DATABASE_URL") {
say "Database configured"
}
env.keys() -> array
Returns an array of all environment variable names.
let all_keys = env.keys()
say len(all_keys)
log
Structured logging with timestamps and severity levels. All log output is written to stderr with ANSI color formatting.
Functions
log.info(…args) -> null
Logs an informational message in green.
log.info("Server started on port", 8080)
// [14:30:15 INFO] Server started on port 8080
log.warn(…args) -> null
Logs a warning message in yellow.
log.warn("Disk usage above 80%")
// [14:30:15 WARN] Disk usage above 80%
log.error(…args) -> null
Logs an error message in red.
log.error("Failed to connect to database:", err)
// [14:30:15 ERROR] Failed to connect to database: connection refused
log.debug(…args) -> null
Logs a debug message in gray. Useful for development diagnostics.
log.debug("Request payload:", data)
// [14:30:15 DEBUG] Request payload: {name: "Alice"}
Notes
- All functions accept any number of arguments. Arguments are converted to strings and joined with spaces.
- Timestamps use the local time in
HH:MM:SSformat. - Output goes to stderr, not stdout, so it does not interfere with piped output.
term
Terminal formatting, colors, and UI widgets. Color functions return styled strings; display functions print to stderr and return null.
Color Functions
Each color function wraps text in ANSI escape codes and returns the styled string.
term.red(text) -> string
term.green(text) -> string
term.blue(text) -> string
term.yellow(text) -> string
term.cyan(text) -> string
term.magenta(text) -> string
say term.red("Error!")
say term.green("Success!")
say term.blue("Info")
term.bold(text) -> string
term.dim(text) -> string
say term.bold("Important")
say term.dim("subtle note")
Display Functions
term.table(rows) -> null
Prints a formatted table from an array of objects. Column widths are auto-calculated. Headers come from the keys of the first object.
let data = [
{ name: "Alice", role: "Admin", active: true },
{ name: "Bob", role: "User", active: false }
]
term.table(data)
// name | role | active
// ------+-------+-------
// Alice | Admin | true
// Bob | User | false
term.hr(width?, char?) -> null
Prints a horizontal rule. Default width is 40, default character is "─".
term.hr() // ────────────────────────────────────────
term.hr(20) // ────────────────────
term.hr(20, "=") // ====================
term.banner(text) -> null
Prints text in a double-line box.
term.banner("Forge v0.3.3")
// ╔════════════════╗
// ║ Forge v0.3.3 ║
// ╚════════════════╝
term.box(text) -> null
Prints text in a single-line box. Supports multi-line text.
term.box("Hello\nWorld")
// ┌───────┐
// │ Hello │
// │ World │
// └───────┘
term.bar(label, value, max?) -> null
Prints a progress bar. Default max is 100.
term.bar("CPU", 73, 100)
// CPU [██████████████████████░░░░░░░░] 73%
term.sparkline(numbers) -> string
Returns a sparkline string from an array of numbers using Unicode block characters.
let spark = term.sparkline([1, 5, 3, 8, 2, 7, 4, 6])
say spark // ▁▅▃█▂▇▃▆
term.gradient(text) -> string
Returns text with a rainbow gradient using 256-color ANSI codes.
say term.gradient("Hello, Forge!")
term.success(message) -> null
Prints a green success message with a checkmark.
term.success("Build complete")
// ✅ Build complete
term.error(message) -> null
Prints a red error message with an X mark.
term.error("Compilation failed")
// ❌ Compilation failed
term.warning(message) -> null
Prints a yellow warning message.
term.warning("Deprecated API usage")
term.info(message) -> null
Prints a cyan info message.
term.info("3 files processed")
term.clear() -> null
Clears the terminal screen.
term.confirm(prompt?) -> bool
Prints a yes/no prompt and returns true if the user enters “y” or “yes”.
if term.confirm("Delete all files?") {
fs.remove("output/")
}
term.menu(options, prompt?) -> any
Displays a numbered menu and returns the selected item.
let choice = term.menu(["New Project", "Open Project", "Quit"])
say choice
term.countdown(seconds?) -> null
Displays an animated countdown. Default is 3 seconds.
term.countdown(5)
term.typewriter(text, delay?) -> null
Prints text one character at a time. Default delay is 30ms per character.
term.typewriter("Loading system...", 50)
term.emoji(name) -> string
Returns an emoji by name. Use term.emojis() to list all available names.
say term.emoji("rocket") // 🚀
say term.emoji("check") // ✅
say term.emoji("fire") // 🔥
term.beep() -> null
Plays the terminal bell sound.
http
HTTP client for making requests and a decorator-based HTTP server built on axum + tokio.
Client Functions
All client functions return a response object with the following fields:
| Field | Type | Description |
|---|---|---|
status | int | HTTP status code |
body | string | Raw response body |
json | any | Parsed JSON body (if applicable) |
headers | object | Response headers |
url | string | Final URL (after redirects) |
time | int | Response time in milliseconds |
method | string | HTTP method used |
http.get(url, options?) -> object
Sends an HTTP GET request.
let resp = http.get("https://api.example.com/users")
say resp.status // 200
say resp.json // [{id: 1, name: "Alice"}, ...]
http.post(url, options?) -> object
Sends an HTTP POST request.
let resp = http.post("https://api.example.com/users", {
body: { name: "Alice", email: "alice@example.com" }
})
say resp.status // 201
http.put(url, options?) -> object
Sends an HTTP PUT request.
let resp = http.put("https://api.example.com/users/1", {
body: { name: "Alice Updated" }
})
http.delete(url, options?) -> object
Sends an HTTP DELETE request.
let resp = http.delete("https://api.example.com/users/1")
say resp.status // 204
http.patch(url, options?) -> object
Sends an HTTP PATCH request.
let resp = http.patch("https://api.example.com/users/1", {
body: { active: false }
})
http.head(url, options?) -> object
Sends an HTTP HEAD request (headers only, no body).
let resp = http.head("https://example.com")
say resp.status
Options Object
| Field | Type | Description |
|---|---|---|
body | any | Request body (auto-serialized as JSON) |
headers | object | Custom request headers |
auth | string | Bearer token (sets Authorization: Bearer <token>) |
timeout | int | Timeout in seconds (default: 30) |
let resp = http.get("https://api.example.com/me", {
auth: "my-secret-token",
headers: { "Accept": "application/json" },
timeout: 10
})
http.download(url, destination?) -> object
Downloads a file from url and saves it to destination. If no destination is provided, the filename is derived from the URL. Returns an object with path, size, and status.
let result = http.download("https://example.com/data.zip", "data.zip")
say result.size // bytes downloaded
http.crawl(url) -> object
Fetches a web page and extracts structured data. Returns an object with:
| Field | Type | Description |
|---|---|---|
url | string | The URL crawled |
status | int | HTTP status code |
title | string | Page title |
description | string | Meta description |
links | array | Array of absolute URLs found in href attributes |
text | string | Visible text content (first 500 characters) |
html_length | int | Total HTML length in characters |
let page = http.crawl("https://example.com")
say page.title
say len(page.links)
http.pretty(response) -> null
Pretty-prints an HTTP response object to stderr with color formatting.
let resp = http.get("https://api.example.com/status")
http.pretty(resp)
Server Decorators
Forge supports declarative HTTP servers using decorators. The @server decorator configures the server, and @get, @post, @put, @delete decorators define route handlers.
@server(port: 3000)
@get("/")
fn index() {
return { message: "Welcome to Forge!" }
}
@get("/users/:id")
fn get_user(id) {
return { id: id, name: "User " + id }
}
@post("/users")
fn create_user(body) {
say "Creating user: " + body.name
return { ok: true, name: body.name }
}
@delete("/users/:id")
fn delete_user(id) {
return { deleted: id }
}
Handler Parameters
Handler functions receive arguments based on parameter names:
- Path parameters (
:id,:name) are passed by matching the parameter name. bodyordatareceives the parsed JSON request body.queryorqsreceives query string parameters as an object.
Server Features
- Built on axum and tokio for production-grade async performance.
- CORS is enabled by default (permissive policy).
- Return values are automatically serialized as JSON responses.
- WebSocket support via the
@wsdecorator.
io
Input/output operations and command-line argument handling.
Functions
io.prompt(text?) -> string
Displays text and reads a line of input from stdin. Returns the input with trailing newline removed.
let name = io.prompt("What is your name? ")
say "Hello, " + name
io.print(…args) -> null
Prints arguments to stdout without a trailing newline. Arguments are joined with spaces.
io.print("Loading")
io.print(".")
io.print(".")
io.print(".\n")
// Loading...
io.args() -> array
Returns all command-line arguments as an array of strings, including the program name.
let args = io.args()
say args // ["forge", "run", "script.fg", "--verbose"]
io.args_parse() -> object
Parses command-line arguments into an object. Flags starting with -- become keys. If a flag is followed by a non-flag value, that value is used; otherwise the flag is set to true.
// forge run script.fg --port 3000 --verbose
let opts = io.args_parse()
say opts["--port"] // "3000"
say opts["--verbose"] // true
io.args_get(flag) -> string | bool | null
Returns the value of a specific command-line flag. Returns true if the flag exists but has no value, or null if the flag is not present.
let port = io.args_get("--port") // "3000" or null
let verbose = io.args_get("--verbose") // true or null
io.args_has(flag) -> bool
Returns true if the flag is present in the command-line arguments.
if io.args_has("--debug") {
log.debug("Debug mode enabled")
}
exec
External command execution. The exec module provides a single function, run_command, which is also available as a top-level builtin.
Functions
run_command(command) -> object
Executes an external command and returns a result object. The command string is split by whitespace into the program name and arguments. The command is not executed through a shell, which prevents shell injection.
Returns:
| Field | Type | Description |
|---|---|---|
stdout | string | Standard output (trailing whitespace trimmed) |
stderr | string | Standard error (trailing whitespace trimmed) |
status | int | Exit code (0 = success) |
ok | bool | true if exit code is 0 |
let result = run_command("ls -la")
say result.stdout
say result.ok // true
let git = run_command("git status")
if git.ok {
say git.stdout
} else {
say "Git error: " + git.stderr
}
Notes
- For shell features (pipes, redirects, globbing), use the
shbuiltin function instead, which executes through a shell. - The command string is split by whitespace, so arguments with spaces are not supported. Use
shfor complex commands.
time
Date, time, and timezone operations. Time objects are plain Forge objects with structured fields. Built on the chrono and chrono-tz crates.
Time Object Structure
Most time functions return a time object with these fields:
| Field | Type | Description |
|---|---|---|
iso | string | ISO 8601 / RFC 3339 timestamp |
unix | int | Unix timestamp in seconds |
unix_ms | int | Unix timestamp in milliseconds |
year | int | Year |
month | int | Month (1-12) |
day | int | Day of month (1-31) |
hour | int | Hour (0-23) |
minute | int | Minute (0-59) |
second | int | Second (0-59) |
weekday | string | Full weekday name (e.g., “Monday”) |
weekday_short | string | Abbreviated weekday (e.g., “Mon”) |
day_of_year | int | Day of the year (1-366) |
timezone | string | Timezone name |
Functions
time.now(timezone?) -> object
Returns the current time as a time object. Defaults to UTC. Pass a timezone string for a specific zone.
let now = time.now()
say now.iso // "2026-03-02T14:30:00+00:00"
say now.year // 2026
let tokyo = time.now("Asia/Tokyo")
say tokyo.hour
time.local() -> object
Returns the current time in the system’s local timezone.
let local = time.local()
say local.timezone // "Local"
time.unix() -> int
Returns the current Unix timestamp in seconds.
let ts = time.unix()
say ts // e.g. 1772618400
time.today() -> string
Returns today’s date as a "YYYY-MM-DD" string.
say time.today() // "2026-03-02"
time.date(year, month, day) -> object
Creates a time object for a specific date at midnight UTC.
let christmas = time.date(2026, 12, 25)
say christmas.weekday // "Friday"
time.parse(input, timezone?) -> object
Parses a date/time string or Unix timestamp into a time object. Supports multiple formats:
"2026-03-02T14:30:00Z"(ISO 8601 with timezone)"2026-03-02T14:30:00"(ISO 8601 without timezone)"2026-03-02 14:30:00"(date + time)"2026-03-02"(date only)"03/02/2026"(US format MM/DD/YYYY)"02.03.2026"(European format DD.MM.YYYY)"Mar 02, 2026"(month name)1772618400(Unix timestamp as integer)
let t = time.parse("2026-03-02")
say t.weekday // "Monday"
let t2 = time.parse(1772618400)
say t2.iso
time.format(time_obj, format_str?) -> string
Formats a time object using a strftime-style format string. Defaults to "%Y-%m-%d %H:%M:%S".
let now = time.now()
say time.format(now) // "2026-03-02 14:30:00"
say time.format(now, "%B %d, %Y") // "March 02, 2026"
say time.format(now, "%H:%M") // "14:30"
time.from_unix(timestamp) -> object
Converts a Unix timestamp (seconds) to a time object.
let t = time.from_unix(0)
say t.iso // "1970-01-01T00:00:00+00:00"
time.diff(t1, t2) -> object
Returns the difference between two time objects.
| Field | Type | Description |
|---|---|---|
seconds | int | Difference in seconds (negative if t1 < t2) |
minutes | float | Difference in minutes |
hours | float | Difference in hours |
days | float | Difference in days |
weeks | float | Difference in weeks |
human | string | Human-readable string (e.g., “2d 3h 15m 0s”) |
let a = time.parse("2026-03-01")
let b = time.parse("2026-03-15")
let d = time.diff(b, a)
say d.days // 14.0
say d.human // "14d 0h 0m 0s"
time.add(time_obj, duration) -> object
Adds a duration to a time object. Duration can be an object with time unit fields or an integer (seconds).
let now = time.now()
let later = time.add(now, { days: 7, hours: 3 })
let also_later = time.add(now, 3600) // add 1 hour in seconds
Duration fields: years, months, weeks, days, hours, minutes, seconds, millis.
time.sub(time_obj, duration) -> object
Subtracts a duration from a time object. Same interface as time.add.
let now = time.now()
let yesterday = time.sub(now, { days: 1 })
time.zone(time_obj, timezone) -> object
Converts a time object to a different timezone.
let utc = time.now()
let eastern = time.zone(utc, "America/New_York")
say eastern.hour
time.zones(filter?) -> array
Returns an array of all available timezone names. Optionally filter by substring.
let all = time.zones()
let us = time.zones("America")
time.is_before(t1, t2) -> bool
Returns true if t1 is before t2.
time.is_after(t1, t2) -> bool
Returns true if t1 is after t2.
time.start_of(time_obj, unit) -> object
Returns the start of the given unit: "day", "hour", "minute", "week", "month", "year".
let now = time.now()
let start_of_day = time.start_of(now, "day")
say start_of_day.hour // 0
time.end_of(time_obj, unit) -> object
Returns the end of the given unit (23:59:59 for days, etc.).
time.sleep(seconds) -> null
Pauses execution for the given number of seconds. Accepts integers or floats.
time.sleep(2) // sleep 2 seconds
time.sleep(0.5) // sleep 500ms
time.elapsed() -> int
Returns the current time in milliseconds since the Unix epoch. Useful for measuring performance.
let start = time.elapsed()
// ... do work ...
let duration = time.elapsed() - start
say "Took " + str(duration) + "ms"
time.is_weekend(time_obj?) -> bool
Returns true if the time falls on Saturday or Sunday. Defaults to now.
time.is_weekday(time_obj?) -> bool
Returns true if the time falls on Monday-Friday. Defaults to now.
time.day_of_week(time_obj?) -> string
Returns the full weekday name. Defaults to now.
time.days_in_month(year?, month?) -> int
Returns the number of days in a month. Accepts (year, month) integers or a time object.
time.days_in_month(2024, 2) // 29 (leap year)
time.days_in_month(2025, 2) // 28
time.is_leap_year(year?) -> bool
Returns true if the given year is a leap year. Accepts an integer or time object.
time.is_leap_year(2024) // true
time.is_leap_year(2025) // false
npc
Fake data generation for testing, prototyping, and seeding databases. “NPC” stands for Non-Player Character – these are the background characters in your application.
Functions
npc.name() -> string
Returns a random full name (first + last).
say npc.name() // e.g. "Luna Nakamura"
npc.first_name() -> string
Returns a random first name from a diverse, gender-neutral pool.
say npc.first_name() // e.g. "Phoenix"
npc.last_name() -> string
Returns a random last name from a globally diverse pool.
say npc.last_name() // e.g. "Patel"
npc.email() -> string
Returns a random email address.
say npc.email() // e.g. "luna.garcia42@proton.me"
npc.username() -> string
Returns a random username in the format adjective_noun123.
say npc.username() // e.g. "turbo_wizard847"
npc.phone() -> string
Returns a random US-format phone number.
say npc.phone() // e.g. "(555) 234-5678"
npc.number(min?, max?) -> int
Returns a random integer. Defaults to range 0-100.
npc.number() // 0-100
npc.number(1, 6) // dice roll
npc.number(1000, 9999) // 4-digit number
npc.pick(array) -> any
Returns a random element from the given array.
let color = npc.pick(["red", "green", "blue"])
say color // e.g. "green"
npc.bool() -> bool
Returns a random boolean.
say npc.bool() // true or false
npc.sentence(word_count?) -> string
Returns a random sentence. Default word count is 5-12.
say npc.sentence() // e.g. "The quick data flows through every node."
say npc.sentence(5) // exactly 5 words
npc.word() -> string
Returns a single random word.
say npc.word() // e.g. "algorithms"
npc.id() -> string
Returns a random UUID-like identifier (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
say npc.id() // e.g. "a3b2f1c8-d4e5-f6a7-b8c9-d0e1f2a3b4c5"
npc.color() -> string
Returns a random hex color code.
say npc.color() // e.g. "#3a7fb2"
npc.ip() -> string
Returns a random IPv4 address.
say npc.ip() // e.g. "192.168.45.12"
npc.url() -> string
Returns a random URL.
say npc.url() // e.g. "https://techflow.io/dashboard"
npc.company() -> string
Returns a random company name.
say npc.company() // e.g. "QuantumLeap"
Example: Seeding a Database
db.open(":memory:")
db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
repeat 100 times {
db.execute("INSERT INTO users (name, email) VALUES (?, ?)", [
npc.name(),
npc.email()
])
}
let users = db.query("SELECT * FROM users LIMIT 5")
term.table(users)
Output Functions
Functions for printing to stdout. All output functions accept any number of arguments, which are converted to strings and joined with spaces.
print(…args) -> null
Prints arguments to stdout without a trailing newline.
print("loading")
print("...")
print("\n")
// loading...
println(…args) -> null
Prints arguments to stdout with a trailing newline.
println("Hello, world!")
println("x =", 42)
// Hello, world!
// x = 42
say(…args) -> null
Alias for println. Prints arguments followed by a newline. This is the idiomatic Forge output function.
say "Hello!"
say "The answer is", 42
yell(…args) -> null
Prints arguments in UPPERCASE followed by a newline.
yell "fire detected"
// FIRE DETECTED
whisper(…args) -> null
Prints arguments in lowercase followed by a newline.
whisper "QUIET PLEASE"
// quiet please
Notes
printandprintlnare classic-style.say,yell, andwhisperare Forge-style.- All five functions write to stdout (not stderr). For stderr output, use the
logmodule. - Arguments of any type are auto-converted to their string representation.
Type Functions
Functions for type conversion and inspection.
str(value) -> string
Converts any value to its string representation.
str(42) // "42"
str(3.14) // "3.14"
str(true) // "true"
str(null) // "null"
str([1, 2]) // "[1, 2]"
int(value) -> int
Converts a value to an integer. Accepts integers, floats (truncated), and numeric strings.
int(3.14) // 3
int("42") // 42
int(100) // 100
Errors if the string is not a valid integer.
float(value) -> float
Converts a value to a float. Accepts integers, floats, and numeric strings.
float(42) // 42.0
float("3.14") // 3.14
float(1) // 1.0
Errors if the string is not a valid number.
type(value) -> string
Returns the type name of value as a string.
type(42) // "Int"
type(3.14) // "Float"
type("hello") // "String"
type(true) // "Bool"
type(null) // "Null"
type([1, 2]) // "Array"
type({a: 1}) // "Object"
type(fn(x) { x }) // "Function"
typeof(value) -> string
Alias for type. Returns the type name of value.
typeof("hello") // "String"
typeof(42) // "Int"
Collection Functions
Functions for working with arrays, objects, and sequences.
len(collection) -> int
Returns the length of a string, array, or object.
len("hello") // 5
len([1, 2, 3]) // 3
len({a: 1, b: 2}) // 2
push(array, value) -> array
Returns a new array with value appended to the end.
let a = [1, 2, 3]
let b = push(a, 4)
say b // [1, 2, 3, 4]
Note:
pushreturns a new array. The original array is not modified.
pop(array) -> array
Returns a new array with the last element removed.
let a = [1, 2, 3]
let b = pop(a)
say b // [1, 2]
keys(object) -> array
Returns an array of the object’s keys as strings.
let obj = { name: "Alice", age: 30 }
say keys(obj) // ["name", "age"]
values(object) -> array
Returns an array of the object’s values.
let obj = { name: "Alice", age: 30 }
say values(obj) // ["Alice", 30]
contains(collection, item) -> bool
Checks if a collection contains an item.
- String, substring: checks if the substring exists in the string.
- Array, value: checks if the value exists in the array.
- Object, key: checks if the key exists in the object.
contains("hello world", "world") // true
contains([1, 2, 3], 2) // true
contains({a: 1}, "a") // true
contains({a: 1}, "b") // false
range(start, end, step?) -> array
Generates an array of integers from start (inclusive) to end (exclusive). Optional step defaults to 1.
range(0, 5) // [0, 1, 2, 3, 4]
range(1, 10, 2) // [1, 3, 5, 7, 9]
range(5, 0, -1) // [5, 4, 3, 2, 1]
enumerate(array) -> array
Returns an array of [index, value] pairs.
let names = ["Alice", "Bob", "Charlie"]
for pair in enumerate(names) {
say str(pair[0]) + ": " + pair[1]
}
// 0: Alice
// 1: Bob
// 2: Charlie
sum(array) -> int | float
Returns the sum of all numeric elements in an array.
sum([1, 2, 3, 4]) // 10
sum([1.5, 2.5]) // 4.0
min_of(array) -> int | float
Returns the minimum value in an array.
min_of([3, 1, 4, 1, 5]) // 1
max_of(array) -> int | float
Returns the maximum value in an array.
max_of([3, 1, 4, 1, 5]) // 5
unique(array) -> array
Returns a new array with duplicate values removed, preserving order.
unique([1, 2, 2, 3, 1]) // [1, 2, 3]
zip(array_a, array_b) -> array
Combines two arrays into an array of [a, b] pairs. Truncates to the shorter array’s length.
zip([1, 2, 3], ["a", "b", "c"])
// [[1, "a"], [2, "b"], [3, "c"]]
flatten(array) -> array
Flattens nested arrays by one level.
flatten([[1, 2], [3, 4], [5]]) // [1, 2, 3, 4, 5]
group_by(array, fn) -> object
Groups array elements by the string returned by fn. Returns an object where keys are group names and values are arrays.
let people = [
{ name: "Alice", dept: "eng" },
{ name: "Bob", dept: "sales" },
{ name: "Charlie", dept: "eng" }
]
let groups = group_by(people, fn(p) { p.dept })
say keys(groups) // ["eng", "sales"]
say groups.eng // [{name: "Alice", dept: "eng"}, {name: "Charlie", dept: "eng"}]
chunk(array, size) -> array
Splits an array into chunks of the given size.
chunk([1, 2, 3, 4, 5], 2)
// [[1, 2], [3, 4], [5]]
slice(array, start, end?) -> array
Returns a sub-array from start (inclusive) to end (exclusive). If end is omitted, slices to the end.
slice([1, 2, 3, 4, 5], 1, 3) // [2, 3]
slice([1, 2, 3, 4, 5], 2) // [3, 4, 5]
partition(array, fn) -> array
Splits an array into two arrays: elements where fn returns truthy, and elements where it returns falsy.
let nums = [1, 2, 3, 4, 5, 6]
let result = partition(nums, fn(n) { n % 2 == 0 })
say result[0] // [2, 4, 6] (even)
say result[1] // [1, 3, 5] (odd)
Functional Operations
map(array, fn) -> array
Applies fn to each element and returns the results.
map([1, 2, 3], fn(x) { x * 2 }) // [2, 4, 6]
filter(array, fn) -> array
Returns elements where fn returns truthy.
filter([1, 2, 3, 4], fn(x) { x > 2 }) // [3, 4]
reduce(array, initial, fn) -> any
Reduces an array to a single value by applying fn(accumulator, element) for each element.
reduce([1, 2, 3, 4], 0, fn(acc, x) { acc + x }) // 10
sort(array, comparator?) -> array
Returns a sorted copy of the array. Without a comparator, sorts numbers numerically and strings alphabetically. The comparator function receives two elements and returns a negative number, zero, or positive number.
sort([3, 1, 4, 1, 5]) // [1, 1, 3, 4, 5]
sort(["banana", "apple", "cherry"]) // ["apple", "banana", "cherry"]
// Custom sort: descending
sort([1, 2, 3], fn(a, b) { b - a }) // [3, 2, 1]
reverse(array) -> array
Returns a reversed copy of the array.
reverse([1, 2, 3]) // [3, 2, 1]
find(array, fn) -> any | null
Returns the first element where fn returns truthy, or null if none match.
find([1, 2, 3, 4], fn(x) { x > 2 }) // 3
find([1, 2], fn(x) { x > 5 }) // null
flat_map(array, fn) -> array
Maps each element with fn and flattens the result by one level.
flat_map([1, 2, 3], fn(x) { [x, x * 10] })
// [1, 10, 2, 20, 3, 30]
any(array, fn) -> bool
Returns true if fn returns truthy for at least one element.
any([1, 2, 3], fn(x) { x > 2 }) // true
any([1, 2, 3], fn(x) { x > 5 }) // false
all(array, fn) -> bool
Returns true if fn returns truthy for every element.
all([2, 4, 6], fn(x) { x % 2 == 0 }) // true
all([2, 3, 6], fn(x) { x % 2 == 0 }) // false
sample(array, n?) -> any | array
Returns a random element (no arguments) or n random elements from the array.
sample([1, 2, 3, 4, 5]) // e.g. 3
sample([1, 2, 3, 4, 5], 2) // e.g. [4, 1]
shuffle(array) -> array
Returns a randomly shuffled copy of the array.
shuffle([1, 2, 3, 4, 5]) // e.g. [3, 5, 1, 4, 2]
String Functions
Functions for string manipulation. All string functions are non-mutating and return new strings.
split(string, delimiter) -> array
Splits a string by the given delimiter.
split("a,b,c", ",") // ["a", "b", "c"]
split("hello world", " ") // ["hello", "world"]
join(array, separator?) -> string
Joins array elements into a string with an optional separator. Default separator is empty string.
join(["a", "b", "c"], ", ") // "a, b, c"
join(["x", "y", "z"]) // "xyz"
replace(string, from, to) -> string
Replaces all occurrences of from with to.
replace("hello world", "world", "Forge") // "hello Forge"
replace("aabbcc", "bb", "XX") // "aaXXcc"
starts_with(string, prefix) -> bool
Returns true if string begins with prefix.
starts_with("hello", "hel") // true
starts_with("hello", "world") // false
ends_with(string, suffix) -> bool
Returns true if string ends with suffix.
ends_with("hello.fg", ".fg") // true
ends_with("hello.fg", ".rs") // false
lines(string) -> array
Splits a string into an array of lines.
lines("first\nsecond\nthird")
// ["first", "second", "third"]
substring(string, start, end?) -> string
Extracts a substring from start (inclusive) to end (exclusive). If end is omitted, extracts to the end of the string.
substring("hello world", 0, 5) // "hello"
substring("hello world", 6) // "world"
index_of(string, search) -> int
Returns the index of the first occurrence of search in string, or -1 if not found.
index_of("hello world", "world") // 6
index_of("hello world", "xyz") // -1
last_index_of(string, search) -> int
Returns the index of the last occurrence of search in string, or -1 if not found.
last_index_of("abcabc", "abc") // 3
last_index_of("hello", "xyz") // -1
pad_start(string, length, char?) -> string
Pads the beginning of a string to reach the target length. Default pad character is a space.
pad_start("42", 5, "0") // "00042"
pad_start("hi", 10) // " hi"
pad_end(string, length, char?) -> string
Pads the end of a string to reach the target length. Default pad character is a space.
pad_end("hi", 10, ".") // "hi........"
pad_end("test", 8) // "test "
capitalize(string) -> string
Returns the string with the first character in uppercase.
capitalize("hello") // "Hello"
capitalize("HELLO") // "HELLO"
title(string) -> string
Returns the string with the first character of each word capitalized.
title("hello world") // "Hello World"
title("the quick brown fox") // "The Quick Brown Fox"
repeat_str(string, count) -> string
Returns the string repeated count times.
repeat_str("ha", 3) // "hahaha"
repeat_str("-", 20) // "--------------------"
count(string, substring) -> int
Counts the number of non-overlapping occurrences of substring in string.
count("banana", "an") // 2
count("hello", "l") // 2
slugify(string) -> string
Converts a string to a URL-friendly slug: lowercase, non-alphanumeric characters replaced with hyphens.
slugify("Hello World!") // "hello-world"
slugify("The Quick Brown Fox") // "the-quick-brown-fox"
snake_case(string) -> string
Converts a string to snake_case. Handles camelCase, PascalCase, and spaces.
snake_case("helloWorld") // "hello_world"
snake_case("MyComponent") // "my_component"
snake_case("some string") // "some_string"
camel_case(string) -> string
Converts a string to camelCase.
camel_case("hello_world") // "helloWorld"
camel_case("some string") // "someString"
Object Functions
Functions for working with Forge objects (key-value maps).
has_key(object, key) -> bool
Returns true if object contains the specified key.
let user = { name: "Alice", age: 30 }
has_key(user, "name") // true
has_key(user, "email") // false
get(object, key, default?) -> any
Retrieves a value from an object by key. Returns default (or null) if the key does not exist. Supports dot-path notation for nested access.
let config = {
db: {
host: "localhost",
port: 5432
}
}
get(config, "db.host") // "localhost"
get(config, "db.port") // 5432
get(config, "db.name", "mydb") // "mydb" (default)
get(config, "missing") // null
Also works with arrays by index:
let data = { items: [10, 20, 30] }
get(data, "items.1") // 20
pick(object, fields) -> object
Returns a new object containing only the specified fields.
let user = { name: "Alice", age: 30, email: "alice@example.com" }
pick(user, ["name", "email"])
// { name: "Alice", email: "alice@example.com" }
omit(object, fields) -> object
Returns a new object with the specified fields removed.
let user = { name: "Alice", age: 30, password: "secret" }
omit(user, ["password"])
// { name: "Alice", age: 30 }
merge(…objects) -> object
Merges multiple objects into one. Later objects override earlier keys.
let defaults = { color: "blue", size: "medium" }
let overrides = { size: "large", weight: 10 }
merge(defaults, overrides)
// { color: "blue", size: "large", weight: 10 }
entries(object) -> array
Returns an array of [key, value] pairs.
let obj = { a: 1, b: 2 }
entries(obj)
// [["a", 1], ["b", 2]]
from_entries(array) -> object
Converts an array of [key, value] pairs into an object. The inverse of entries.
from_entries([["name", "Alice"], ["age", 30]])
// { name: "Alice", age: 30 }
diff(object_a, object_b) -> object
Returns an object describing the differences between two objects. Each key that differs contains an object with a and b values.
let old = { name: "Alice", age: 30, city: "NYC" }
let new_val = { name: "Alice", age: 31, email: "a@b.com" }
diff(old, new_val)
// {
// age: { a: 30, b: 31 },
// city: { a: "NYC", b: null },
// email: { a: null, b: "a@b.com" }
// }
Shell Functions
Functions for executing shell commands and interacting with the operating system. Unlike run_command, these functions execute through a system shell (/bin/sh on Unix, cmd on Windows), so pipes, redirects, and globbing work.
sh(command) -> string
Executes a shell command and returns stdout as a string. Errors if the command fails.
let files = sh("ls -la")
say files
let count = sh("wc -l < data.txt")
say count
shell(command) -> object
Executes a shell command and returns a detailed result object with stdout, stderr, status, and ok fields.
let result = shell("git status")
if result.ok {
say result.stdout
} else {
say "Error: " + result.stderr
}
sh_lines(command) -> array
Executes a shell command and returns stdout split into an array of lines.
let files = sh_lines("ls *.fg")
for file in files {
say "Found: " + file
}
sh_json(command) -> any
Executes a shell command and parses stdout as JSON.
let config = sh_json("cat package.json")
say config.name
say config.version
sh_ok(command) -> bool
Executes a shell command and returns true if the exit code is 0.
if sh_ok("which python3") {
say "Python 3 is installed"
}
which(program) -> string | null
Returns the full path to program, or null if not found. Equivalent to the Unix which command.
which("node") // "/usr/local/bin/node"
which("foobar") // null
cwd() -> string
Returns the current working directory as a string.
say cwd() // "/home/alice/project"
cd(path) -> null
Changes the current working directory.
cd("/tmp")
say cwd() // "/tmp"
pipe_to(command, input) -> string
Pipes input as stdin to the given command and returns stdout.
let sorted = pipe_to("sort", "banana\napple\ncherry")
say sorted
// apple
// banana
// cherry
Assertion Functions
Functions for testing and validation. Assertion failures produce runtime errors with descriptive messages.
assert(condition, message?) -> null
Asserts that condition is truthy. Throws a runtime error if false.
assert(1 + 1 == 2)
assert(len("hello") > 0, "string should not be empty")
assert_eq(actual, expected, message?) -> null
Asserts that actual equals expected. Shows both values on failure.
assert_eq(1 + 1, 2)
assert_eq(sort([3, 1, 2]), [1, 2, 3])
assert_eq(user.name, "Alice", "user name mismatch")
assert_ne(actual, expected, message?) -> null
Asserts that actual does not equal expected.
assert_ne(1, 2)
assert_ne(user.role, "admin", "user should not be admin")
assert_throws(fn, message?) -> null
Asserts that calling fn produces a runtime error. Useful for testing error handling.
assert_throws(fn() { int("not a number") })
assert_throws(fn() { bruh "expected crash" })
satisfies(value, interface) -> bool
Checks whether value structurally satisfies an interface (Go-style structural typing). Returns true if the value has all methods specified by the interface, either through the environment or through give/impl blocks.
power Printable {
fn to_string() -> string
}
thing User {
name: string
}
give User {
fn to_string() {
return self.name
}
}
let u = User { name: "Alice" }
assert(satisfies(u, Printable))
See the Type System chapter for more details on interface satisfaction.
GenZ Debug Kit
A set of debugging and assertion functions with personality. These are fully functional tools with expressive error messages – not just jokes.
sus(value) -> value
Inspects a value and prints its type and content to stderr, then returns the value unchanged. Works like Rust’s dbg! macro – you can wrap any expression without changing program behavior.
let x = sus(42)
// stderr: 🔍 SUS CHECK: 42 (Int)
// x is still 42
let result = sus(http.get("https://api.example.com"))
// Prints the response object, then returns it
bruh(message?) -> never
Panics with a runtime error. Equivalent to panic! in Rust. Default message: “something ain’t right”.
bruh "database connection lost"
// Error: BRUH: database connection lost
bruh
// Error: BRUH: something ain't right
bet(condition, message?) -> bool
Asserts that condition is truthy. Returns true on success, errors on failure. Equivalent to assert.
bet(user.age >= 18, "user must be an adult")
// On failure: Error: LOST THE BET: user must be an adult
bet(1 + 1 == 2) // passes, returns true
no_cap(a, b) -> bool
Asserts that a equals b. Returns true on success, errors on failure. Equivalent to assert_eq.
no_cap(1 + 1, 2) // passes
no_cap("hello", "world")
// Error: CAP DETECTED: hello ≠ world
ick(condition, message?) -> bool
Asserts that condition is false. Returns true when the condition is false, errors when true. The inverse of bet.
ick(user.banned, "user should not be banned")
// On failure: Error: ICK: user should not be banned
ick(false) // passes, returns true
Execution Helpers
Higher-order functions for profiling, error handling, and benchmarking.
cook(fn) -> any
Executes fn, measures its execution time, and prints a performance report to stderr. Returns the function’s return value.
let result = cook(fn() {
let total = 0
for i in range(0, 1000000) {
total = total + i
}
return total
})
// stderr: 👨🍳 COOKED: done in 42.15ms — it's giving adequate
// result contains the computed total
Performance messages vary by duration:
- Under 1ms: “speed demon fr”
- 1-100ms: “no cap that was fast”
- 100ms-1s: “it’s giving adequate”
- Over 1s: “bruh that took a minute”
yolo(fn) -> any | None
Executes fn and swallows all errors. Returns the function’s result on success, or None on failure. Useful for non-critical operations where errors can be safely ignored.
let data = yolo(fn() {
return fs.read("maybe-missing.txt")
})
// data is the file contents or None
if is_none(data) {
say "File not found, using defaults"
}
ghost(fn) -> any
Executes fn silently. The function runs normally and its return value is passed through, but intended for cases where you want to suppress side effects.
let result = ghost(fn() {
return compute_something()
})
slay(fn, iterations?) -> object
Benchmarks fn by running it iterations times (default: 100). Prints a summary to stderr and returns a statistics object.
Returns:
| Field | Type | Description |
|---|---|---|
avg_ms | float | Average time per iteration in milliseconds |
min_ms | float | Minimum time in milliseconds |
max_ms | float | Maximum time in milliseconds |
p99_ms | float | 99th percentile time in milliseconds |
runs | int | Number of iterations |
result | any | Return value of the last iteration |
let stats = slay(fn() {
return math.pow(2, 20)
}, 1000)
// stderr: 💅 SLAYED: 1000x runs — avg 0.003ms, min 0.001ms, max 0.012ms, p99 0.008ms
say stats.avg_ms // 0.003
say stats.runs // 1000
say stats.result // 1048576
Dual Syntax Philosophy
Principle
Every construct in Forge has two spellings: a classic form familiar to programmers coming from C, Rust, or JavaScript, and a natural form that reads closer to English. Both forms compile to the exact same AST and execute identically. There is no performance difference, no feature difference, and no hidden cost.
Why Dual Syntax?
Programming languages force a false choice: be accessible or be powerful. Forge rejects this tradeoff.
- Classic syntax serves experienced developers who think in
fn,let, andelse. It is terse and precise. - Natural syntax serves learners, scripters, and domain experts who prefer
define,set, andotherwise. It reduces the cognitive barrier to writing code.
Neither form is “training wheels.” Both are first-class citizens of the language.
Rules
- Both forms are always available. No mode switches, no compiler flags, no feature gates.
- They can be mixed freely. You can use
leton one line andseton the next. A file can usefnfor one function anddefinefor another. - The AST is identical. The parser recognizes both spellings and produces the same node.
let x = 5andset x to 5produce an identicalLetDeclAST node. - Error messages normalize. Runtime errors use the classic form regardless of which syntax the programmer used.
- Formatting preserves choice.
forge fmtdoes not convert between forms. Your stylistic choice is respected.
Example: Mixed Syntax
// Classic variable, natural function
let name = "Alice"
define greet(person) {
say "Hello, " + person
}
// Natural variable, classic function
set age to 30
fn is_adult(a) {
return a >= 18
}
// Both work together seamlessly
if is_adult(age) {
greet(name)
} otherwise {
say "Too young"
}
Design Guideline
When in doubt, use whichever form your team agrees on. For library code shared publicly, classic syntax is conventional. For scripts, tutorials, and learning materials, natural syntax often reads better.
Syntax Mapping
Complete mapping between classic and natural forms. Both compile to identical AST nodes.
Variables
| Classic | Natural | Description |
|---|---|---|
let x = 5 | set x to 5 | Immutable binding |
let mut x = 0 | set mut x to 0 | Mutable binding |
x = 10 | change x to 10 | Reassignment |
let {a, b} = obj | unpack {a, b} from obj | Destructuring |
// Classic
let name = "Alice"
let mut count = 0
count = count + 1
// Natural
set name to "Alice"
set mut count to 0
change count to count + 1
Functions
| Classic | Natural | Description |
|---|---|---|
fn add(a, b) { } | define add(a, b) { } | Function definition |
async fn fetch() { } | forge fetch() { } | Async function |
return value | return value | Return (same in both) |
// Classic
fn greet(name) {
return "Hello, " + name
}
// Natural
define greet(name) {
return "Hello, " + name
}
Control Flow
| Classic | Natural | Description |
|---|---|---|
else { } | otherwise { } | Else branch |
else { } | nah { } | Else branch (casual) |
else if | otherwise if | Else-if branch |
// Classic
if x > 0 {
say "positive"
} else if x == 0 {
say "zero"
} else {
say "negative"
}
// Natural
if x > 0 {
say "positive"
} otherwise if x == 0 {
say "zero"
} nah {
say "negative"
}
Output
| Classic | Natural | Description |
|---|---|---|
println("text") | say "text" | Print with newline |
print("text") | print("text") | Print without newline |
| – | yell "text" | Print uppercased |
| – | whisper "text" | Print lowercased |
Types and Structures
| Classic | Natural | Description |
|---|---|---|
struct User { } | thing User { } | Struct definition |
impl User { } | give User { } | Method implementation |
interface Printable { } | power Printable { } | Interface definition |
enum Color { } | craft Color { } | Enum definition |
// Classic
struct User {
name: string,
age: int
}
impl User {
fn greet(self) {
say "Hi, I'm " + self.name
}
}
// Natural
thing User {
name: string,
age: int
}
give User {
fn greet(self) {
say "Hi, I'm " + self.name
}
}
Async / Concurrency
| Classic | Natural | Description |
|---|---|---|
async fn x() { } | forge x() { } | Async function |
await expr | hold expr | Await an async value |
yield value | emit value | Yield from a generator |
fetch("url") | grab resp from "url" | HTTP fetch |
// Classic
async fn get_data() {
let resp = await fetch("https://api.example.com/data")
return resp
}
// Natural
forge get_data() {
let resp = hold grab data from "https://api.example.com/data"
return resp
}
Pattern Matching
| Classic | Natural |
|---|---|
match value { } | match value { } |
when value { } | when value { } |
Both match and when are available; when supports guard-style syntax unique to Forge (see Innovation Keywords).
Modules
| Classic | Natural | Description |
|---|---|---|
has InterfaceName | has InterfaceName | Interface conformance assertion |
The has keyword asserts that a type satisfies an interface at the point of declaration.
Innovation Keywords
Keywords unique to Forge that have no direct equivalent in other mainstream languages. These are not aliases – they introduce genuinely new constructs.
when Guards
Pattern matching with comparison guards. Unlike match, when tests a single value against comparison operators.
let score = 85
let grade = when score {
>= 90 -> "A",
>= 80 -> "B",
>= 70 -> "C",
>= 60 -> "D",
else -> "F"
}
say grade // "B"
The else arm is required and handles any unmatched case.
must
Unwraps a Result or crashes with a clear error message. Used when failure is unrecoverable.
let data = must fs.read("config.json")
// If the file doesn't exist, the program crashes with a descriptive error
safe
Null-safe execution block. If any expression inside safe would error, the block evaluates to null instead of crashing. Statement-only (cannot be used as an expression).
safe {
let data = fs.read("maybe-missing.txt")
say data
}
// If file is missing, execution continues silently
check … is not empty
Declarative validation. Checks a condition and produces a validation error if it fails.
check name is not empty
check age >= 0
check email contains "@"
retry N times
Automatically retries a block up to N times on failure.
retry 3 times {
let resp = http.get("https://flaky-api.example.com/data")
say resp.json
}
If all retries fail, the error from the last attempt is raised.
timeout N seconds
Limits execution time for a block. If the block does not complete within the time limit, it is interrupted.
timeout 5 seconds {
let result = http.get("https://slow-api.example.com")
say result.json
}
Note: This feature is experimental and may not interrupt all operations cleanly.
schedule every N units
Runs a block repeatedly on a schedule (cron-like).
schedule every 5 minutes {
let status = http.get("https://api.example.com/health")
if status.status != 200 {
log.error("Health check failed!")
}
}
Supported units: seconds, minutes, hours.
watch “path”
Monitors a file or directory for changes and executes the block when changes are detected.
watch "src/" {
say "Files changed, rebuilding..."
sh("cargo build")
}
ask “prompt”
Sends a prompt to an AI/LLM and returns the response. Requires AI configuration.
let answer = ask "What is the capital of France?"
say answer // "Paris"
download “url” to “file”
Downloads a file from a URL and saves it to disk. Syntax sugar for http.download.
download "https://example.com/data.csv" to "data.csv"
crawl “url”
Fetches and parses a web page, returning structured data. Syntax sugar for http.crawl.
let page = crawl "https://example.com"
say page.title
say page.links
repeat N times
Executes a block exactly N times. A counted loop without a loop variable.
repeat 5 times {
say "Hello!"
}
wait N units
Pauses execution for the specified duration.
wait 2 seconds
wait 500 milliseconds
Supported units: seconds, milliseconds, minutes.
grab … from “url”
Natural syntax for HTTP fetch. Assigns the response to a variable.
grab data from "https://api.example.com/users"
say data
emit value
Yields a value from a generator function. Natural equivalent of yield.
fn fibonacci() {
let a = 0
let b = 1
loop {
emit a
let temp = a + b
a = b
b = temp
}
}
hold expr
Awaits an async expression. Natural equivalent of await.
forge fetch_data() {
let resp = hold http.get("https://api.example.com")
return resp.json
}
Execution Model
Forge provides three execution tiers, selectable at the command line. All tiers accept the same source files; they differ in feature coverage, performance, and implementation strategy.
Three Tiers
| Tier | Flag | Implementation | Performance | Feature Coverage |
|---|---|---|---|---|
| Interpreter | (default) | Tree-walking | Baseline | Full (100%) |
| Bytecode VM | --vm | Register-based VM | ~10x faster than interpreter | Partial (~60%) |
| JIT Compiler | --jit | Cranelift native code | ~50-100x faster than interpreter | Minimal (~30%) |
forge run program.fg # Interpreter (default)
forge run program.fg --vm # Bytecode VM
forge run program.fg --jit # JIT compiler
When to Use Each Tier
Interpreter (Default)
Use the interpreter for:
- All general-purpose development
- HTTP servers (
@server,@get,@post) - Database access (
db,pg) - AI integration (
ask) - File system, crypto, terminal UI
- Any code using the full standard library
The interpreter supports every feature of the language. It is the reference implementation.
Bytecode VM (--vm)
Use the VM for:
- Compute-intensive loops and numerical work
- Programs that primarily use
math,fs,io, and basic control flow - Benchmarking against the interpreter
The VM compiles Forge source to a register-based bytecode and executes it in a virtual machine with mark-sweep garbage collection. It does not support HTTP servers, database connections, or several stdlib modules.
JIT Compiler (--jit)
Use the JIT for:
- Maximum performance on hot numerical code
- Benchmarking (e.g.,
fib(30)runs 11x faster than Python) - Functions that are purely computational
The JIT compiles hot functions to native machine code via Cranelift. It supports the smallest subset of the language – primarily arithmetic, function calls, and basic control flow.
Trade-off Summary
Feature Coverage: Interpreter > VM > JIT
Performance: JIT > VM > Interpreter
Startup Time: Interpreter < VM < JIT
The interpreter is always the safe default. Switch to --vm or --jit only when you need the performance and have verified your program works on that tier.
Interpreter
The Forge interpreter is a tree-walking interpreter implemented in Rust. It traverses the AST directly, evaluating each node as it encounters it. This is the default and most complete execution engine.
Architecture
Source (.fg) -> Lexer -> Parser -> AST -> Interpreter -> Result
|
Environment (scopes)
|
Runtime Bridge
(axum, reqwest, tokio, rusqlite)
The interpreter lives in src/interpreter/mod.rs (~8,100 lines) and is the largest single file in the codebase.
Key Components
Environment
The interpreter maintains a stack of scopes. Each scope is an IndexMap<String, Value> that maps names to values. Variable resolution walks the scope stack from innermost to outermost.
- Global scope: Pre-populated with all 16 stdlib modules and all built-in functions.
- Function scope: Created on each function call, closed over by lambdas.
- Block scope: Created for
if,for,while, and other block statements.
Value Type
The Value enum represents all runtime values:
Int(i64)– 64-bit integerFloat(f64)– 64-bit floatBool(bool)– booleanString(String)– heap-allocated stringArray(Vec<Value>)– dynamic arrayObject(IndexMap<String, Value>)– ordered key-value mapNull– null valueFunction { params, body, closure }– named function with captured environmentLambda { params, body, closure }– anonymous functionBuiltIn(String)– reference to a built-in function by nameResultOk(Box<Value>)/ResultErr(Box<Value>)– Result typeSome(Box<Value>)/None– Option typeChannel(Arc<ChannelInner>)– concurrency channelTaskHandle(Arc<TaskInner>)– async task handle
Dispatch
Built-in function dispatch is a single large match statement in call_builtin. When a BuiltIn("name") value is called, the interpreter matches on the name string and executes the corresponding Rust code.
Stdlib module functions (e.g., math.sqrt) are dispatched through the module’s call function. The interpreter detects dot-access on a module object and routes the call to the appropriate module.
Features Unique to the Interpreter
The following features are only available in the interpreter tier:
- HTTP server (
@server,@get,@post,@delete,@ws) - Database access (
db.open,db.query,pg.connect) - AI integration (
ask) - Web scraping (
crawl) - File download (
download ... to) - Terminal UI widgets (
term.table,term.menu,term.confirm) - GenZ debug kit (
sus,bruh,bet,no_cap,ick) - Execution helpers (
cook,yolo,ghost,slay) - Concurrency (
channel,send,receive,spawn)
Performance Characteristics
The tree-walking approach means the interpreter re-traverses the AST on every loop iteration and function call. This makes it approximately 20x slower than Python for deep recursion benchmarks like fib(35).
For most real-world scripts (file processing, HTTP handlers, database queries), interpreter overhead is negligible compared to I/O latency. The interpreter is the recommended tier for all general-purpose work.
Bytecode VM
The Forge bytecode VM is a register-based virtual machine that compiles Forge source to 32-bit instructions and executes them in a loop. It provides significantly better performance than the tree-walking interpreter at the cost of reduced feature coverage.
Invocation
forge run program.fg --vm
Architecture
Source -> Lexer -> Parser -> AST -> Compiler -> Bytecode Chunks -> Machine -> Result
|
Mark-Sweep GC
|
Green Threads
Key source files:
src/vm/compiler.rs(~927 lines) – AST to bytecode compilationsrc/vm/machine.rs(~2,483 lines) – bytecode execution enginesrc/vm/bytecode.rs– instruction set definitionsrc/vm/gc.rs– mark-sweep garbage collectorsrc/vm/frame.rs– call frame managementsrc/vm/value.rs– VM-specific value typesrc/vm/green.rs– green thread scheduler
Instruction Encoding
All instructions are 32 bits wide. Three encoding formats:
ABC Format: [op:8][a:8][b:8][c:8]
Used for register-to-register operations. a is typically the destination register; b and c are source registers.
ABx Format: [op:8][a:8][bx:16]
Used for instructions with a larger operand, such as constant loading. bx is an unsigned 16-bit index.
AsBx Format: [op:8][a:8][sbx:16]
Used for jump instructions. sbx is a signed 16-bit offset stored as unsigned (with bias).
Important: The VM pre-increments IP before applying jump offsets. The JIT target address is
ip + 1 + sbx, notip + sbx.
Register Machine
The VM uses a register-based architecture rather than a stack-based one. Each call frame has its own register window. Registers are addressed by 8-bit indices, allowing up to 256 registers per frame.
Benefits:
- Fewer instructions than a stack VM (no push/pop for every operand)
- Better cache locality for register access
- Natural fit for the JIT tier
Constant Pool
Each compiled function (called a “Chunk”) has a constant pool for literals, strings, and function prototypes. Constants are deduplicated via identical() comparison to avoid wasting pool slots.
Garbage Collection
The VM uses a mark-sweep garbage collector. Heap-allocated objects (strings, arrays, objects, closures) are tracked by the GC. Collection is triggered when the allocation count exceeds a threshold.
The mark phase walks from GC roots (registers, global environment, call stack). The sweep phase frees unreachable objects.
Green Threads
The VM includes a cooperative green thread scheduler (src/vm/green.rs). Green threads are multiplexed over a single OS thread with explicit yield points.
Supported Features
The VM supports core language features:
- Variables, functions, closures
- Control flow (if/else, for, while, match)
- Arrays and objects
- Arithmetic and comparison operators
- String operations
math,fs,io,npcmodules- Basic built-in functions
Unsupported Features
The following require the interpreter:
- HTTP server and client
- Database connections
- AI integration
- Terminal UI widgets
- Most execution helpers
JIT Compiler
The Forge JIT compiler translates hot bytecode functions into native machine code using the Cranelift code generator. It provides the highest performance tier, achieving approximately 11x faster execution than Python on recursive benchmarks like fib(30).
Invocation
forge run program.fg --jit
Architecture
Source -> Lexer -> Parser -> AST -> Compiler -> Bytecode -> JIT -> Native Code
|
Cranelift IR
|
Machine Code (x86_64/AArch64)
Key source files:
src/vm/jit/ir_builder.rs(~276 lines) – Bytecode to Cranelift IR translationsrc/vm/jit/jit_module.rs(~47 lines) – JIT module management
How It Works
- The program is first compiled to bytecode (same as the
--vmpath). - Functions selected for JIT compilation are translated from bytecode into Cranelift’s intermediate representation (IR).
- Cranelift compiles the IR to native machine code for the host architecture.
- The native code is loaded into memory and called directly, bypassing the bytecode interpreter.
Cranelift IR Translation
The IR builder walks the bytecode instruction stream and emits Cranelift IR operations:
- Arithmetic bytecodes (
ADD,SUB,MUL,DIV) map to Craneliftiadd,isub,imul,sdiv. - Comparison bytecodes map to Cranelift
icmpwith the appropriate condition. - Jump bytecodes map to Cranelift branch and block terminators.
- Function calls generate Cranelift
callinstructions.
Jump Offset Encoding
The VM pre-increments the instruction pointer (IP) before applying jump offsets. When translating jumps to Cranelift blocks, the JIT target is calculated as:
target = ip + 1 + sbx
This is a critical detail. Using ip + sbx produces incorrect branch targets.
Performance
On fib(30):
| Engine | Time | Relative |
|---|---|---|
| Python 3 | ~330ms | 1x |
| Forge interpreter | ~6,600ms | 0.05x |
| Forge VM | ~660ms | 0.5x |
| Forge JIT | ~30ms | 11x |
The JIT excels at tight numerical loops and recursive functions where the overhead of interpretation dominates.
Supported Features
The JIT supports the most restricted subset:
- Integer and float arithmetic
- Function calls and recursion
- Basic control flow (if/else, loops)
- Local variables
- Comparisons and boolean logic
Limitations
- No string operations
- No object or array construction
- No standard library access
- No closures or higher-order functions
- No error handling (try/catch, Result)
- Compilation overhead makes it unsuitable for short-running programs
- Only beneficial for compute-heavy inner loops
Platform Support
Cranelift supports:
- x86_64 (macOS, Linux, Windows)
- AArch64 / ARM64 (macOS Apple Silicon, Linux)
HTTP Server
Forge includes a built-in HTTP server powered by axum and tokio. Servers are defined declaratively using decorators and launched automatically when the interpreter detects a @server directive.
Architecture
Forge Source
|
v
Parser extracts decorators (@server, @get, @post, ...)
|
v
runtime/server.rs builds axum Router
|
v
axum + tokio serve requests
|
v
Each request locks the Interpreter mutex,
calls the handler function, serializes the return value as JSON
The server implementation lives in src/runtime/server.rs (~354 lines).
Server Configuration
The @server decorator configures the server:
@server(port: 3000, host: "0.0.0.0")
| Parameter | Type | Default | Description |
|---|---|---|---|
port | int | 8080 | Listen port |
host | string | “127.0.0.1” | Bind address |
Route Decorators
@get(path?)
Registers a function as a GET handler.
@get("/users")
fn list_users() {
return [{ name: "Alice" }, { name: "Bob" }]
}
@post(path?)
Registers a function as a POST handler.
@post("/users")
fn create_user(body) {
say "Creating: " + body.name
return { ok: true }
}
@put(path?)
Registers a function as a PUT handler.
@delete(path?)
Registers a function as a DELETE handler.
@ws(path?)
Registers a function as a WebSocket handler. The function receives each incoming message as a string and returns a response string.
@ws("/chat")
fn handle_message(msg) {
return "Echo: " + msg
}
Path Parameters
Path parameters use colon syntax (:param). They are automatically mapped to function parameters by name.
@get("/users/:id")
fn get_user(id) {
return { id: id, name: "User " + id }
}
// GET /users/42 -> { id: "42", name: "User 42" }
The Forge path syntax (:id) is internally converted to axum’s brace syntax ({id}).
Handler Parameters
Handler functions receive arguments based on their parameter names:
| Parameter Name | Source |
|---|---|
| Name matching a path param | URL path parameter |
body or data | Parsed JSON request body |
query or qs | Query string as an object |
| Name matching a query param | Individual query parameter |
| Other | null |
@post("/search")
fn search(body, query) {
say "Search body: " + str(body)
say "Query params: " + str(query)
return { results: [] }
}
JSON Serialization
Return values from handlers are automatically serialized to JSON:
| Forge Type | JSON |
|---|---|
int | number |
float | number |
bool | boolean |
string | string |
null | null |
array | array |
object | object |
ResultOk(v) | {"Ok": v} |
ResultErr(v) | {"Err": v} |
| Other | "<TypeName>" |
Error Handling
If a handler function throws a runtime error, the server returns HTTP 500 with:
{ "error": "error message here" }
CORS
CORS is enabled by default with a permissive policy (all origins, all methods, all headers). This is suitable for development; production deployments should add appropriate restrictions at the reverse proxy level.
Concurrency Model
The interpreter is wrapped in an Arc<Mutex<Interpreter>>. Each incoming request acquires the mutex lock, calls the handler, and releases it. This means handlers execute serially – only one request is processed at a time.
For high-throughput scenarios, consider running multiple Forge server instances behind a load balancer.
Memory Model
Forge uses different memory management strategies depending on the execution tier.
Interpreter Memory
The interpreter uses Rust’s standard memory management with no garbage collector.
Value Representation
All Forge values are represented by the Value enum in Rust. Heap-allocated variants:
| Value | Heap Allocation |
|---|---|
String(String) | Rust String (heap-allocated, growable) |
Array(Vec<Value>) | Vec on the heap |
Object(IndexMap<String, Value>) | IndexMap on the heap |
Function { closure, ... } | Captured environment on the heap |
Lambda { closure, ... } | Captured environment on the heap |
Channel(Arc<ChannelInner>) | Reference-counted channel |
TaskHandle(Arc<TaskInner>) | Reference-counted task |
Primitive types (Int, Float, Bool, Null) are stored inline without heap allocation.
Ownership and Cloning
The interpreter clones values when:
- Passing arguments to functions
- Returning values from functions
- Assigning variables
- Indexing into arrays or objects
This is a simple, correct approach that avoids shared mutable state. The trade-off is increased memory allocation for large data structures.
Reference Counting
Channels and task handles use Arc (atomic reference counting) for shared ownership across threads. When the last reference is dropped, the resource is freed.
No Manual Memory Management
Forge programs never explicitly allocate or free memory. There is no malloc, free, new, or delete. Memory is managed entirely by the Rust runtime.
VM Memory
The bytecode VM uses a mark-sweep garbage collector for heap-allocated objects.
VM Value Representation
The VM has its own Value enum (in src/vm/value.rs) optimized for the register-based architecture:
| Value | Representation |
|---|---|
Int(i64) | Inline 64-bit integer |
Float(f64) | Inline 64-bit float |
Bool(bool) | Inline boolean |
Null | Inline null marker |
Obj(usize) | Index into the GC heap |
Heap-allocated objects are stored in the GC’s object table and referenced by index.
Object Kinds
ObjKind::String(String)
ObjKind::Array(Vec<Value>)
ObjKind::Object(IndexMap<String, Value>)
ObjKind::Closure { ... }
GC Algorithm
The mark-sweep collector works in two phases:
- Mark: Starting from roots (registers, global environment, call stack frames), traverse all reachable objects and set their mark bit.
- Sweep: Walk the object table. Free any object without a mark bit. Clear all mark bits for the next cycle.
Collection is triggered when the number of allocated objects exceeds a dynamic threshold that grows as the program allocates more objects.
GC Roots
The following locations are scanned as roots:
- All registers in the current and parent call frames
- The global environment
- Constant pools of active chunks
- Green thread stacks
JIT Memory
The JIT compiler allocates executable memory pages for generated native code via Cranelift’s JITModule. This memory is mapped with execute permissions and is freed when the JIT module is dropped.
JIT-compiled functions operate on machine registers and stack-allocated values. There is no GC interaction – the JIT tier does not support heap-allocated objects.
Grammar
Simplified EBNF grammar for the Forge language. This is a reference guide, not a formal specification. Optional elements are marked with ?, repetition with *, and alternation with |.
Program
program = statement* EOF ;
Statements
statement = let_stmt
| assign_stmt
| fn_def
| struct_def
| interface_def
| impl_block
| type_def
| if_stmt
| match_stmt
| when_stmt
| for_stmt
| while_stmt
| loop_stmt
| return_stmt
| break_stmt
| continue_stmt
| try_catch
| import_stmt
| spawn_stmt
| destructure_stmt
| check_stmt
| safe_block
| timeout_block
| retry_block
| schedule_block
| watch_block
| prompt_def
| decorator_stmt
| yield_stmt
| expression_stmt ;
Variable Declarations
let_stmt = ( "let" | "set" ) "mut"? IDENT ( ":" type_ann )? ( "=" | "to" ) expr NEWLINE ;
assign_stmt = ( IDENT | field_access | index_expr ) ( "=" | "+=" | "-=" | "*=" | "/=" ) expr NEWLINE
| "change" IDENT "to" expr NEWLINE ;
destructure_stmt = ( "let" | "unpack" ) destruct_pattern ( "=" | "from" ) expr NEWLINE ;
destruct_pattern = "{" IDENT ( "," IDENT )* "}"
| "[" IDENT ( "," IDENT )* ( "," "..." IDENT )? "]" ;
Function Definitions
fn_def = decorator* ( "fn" | "define" | "async" "fn" | "forge" ) IDENT "(" param_list? ")" ( "->" type_ann )? block ;
param_list = param ( "," param )* ;
param = IDENT ( ":" type_ann )? ( "=" expr )? ;
block = "{" statement* "}" ;
Struct / Thing Definitions
struct_def = ( "struct" | "thing" ) IDENT "{" field_def* "}" ;
field_def = IDENT ":" type_ann ( "=" expr )? NEWLINE ;
Interface / Power Definitions
interface_def = ( "interface" | "power" ) IDENT "{" method_sig* "}" ;
method_sig = IDENT "(" param_list? ")" ( "->" type_ann )? NEWLINE ;
Impl / Give Blocks
impl_block = "impl" IDENT "{" fn_def* "}"
| "give" IDENT "{" fn_def* "}"
| "give" IDENT "the" "power" IDENT "{" fn_def* "}" ;
Type Definitions (ADTs)
type_def = "type" IDENT "=" variant ( "|" variant )* ;
variant = IDENT ( "(" type_ann ( "," type_ann )* ")" )? ;
Control Flow
if_stmt = "if" expr block ( ( "else" | "otherwise" | "nah" ) ( if_stmt | block ) )? ;
match_stmt = "match" expr "{" match_arm* "}" ;
match_arm = pattern "->" ( expr | block ) ","? ;
pattern = "_"
| literal
| IDENT
| IDENT "(" pattern ( "," pattern )* ")" ;
when_stmt = "when" expr "{" when_arm* "}" ;
when_arm = ( comparison_op expr | "else" ) "->" expr ","? ;
for_stmt = "for" IDENT ( "," IDENT )? "in" expr block
| "for" "each" IDENT "in" expr block ;
while_stmt = "while" expr block ;
loop_stmt = "loop" block ;
return_stmt = "return" expr? NEWLINE ;
break_stmt = "break" NEWLINE ;
continue_stmt = "continue" NEWLINE ;
yield_stmt = ( "yield" | "emit" ) expr NEWLINE ;
Error Handling
try_catch = "try" block "catch" IDENT block ;
Import
import_stmt = "import" STRING
| "from" STRING "import" IDENT ( "," IDENT )* ;
Spawn
spawn_stmt = "spawn" block ;
Innovation Statements
check_stmt = "check" expr ( "is" "not"? "empty" | "contains" expr | "between" expr "and" expr ) ;
safe_block = "safe" block ;
timeout_block = "timeout" expr "seconds" block ;
retry_block = "retry" expr "times" block ;
schedule_block = "schedule" "every" expr ( "seconds" | "minutes" ) block ;
watch_block = "watch" expr block ;
Decorators
decorator = "@" IDENT ( "(" decorator_args? ")" )? ;
decorator_args = decorator_arg ( "," decorator_arg )* ;
decorator_arg = IDENT ":" expr
| expr ;
Prompt Definitions
prompt_def = "prompt" IDENT "(" param_list? ")" "{" prompt_body "}" ;
prompt_body = ( "system" ":" STRING )? "user" ":" STRING ( "returns" ":" STRING )? ;
Expressions
expr = or_expr ;
or_expr = and_expr ( "||" and_expr )* ;
and_expr = equality ( "&&" equality )* ;
equality = comparison ( ( "==" | "!=" ) comparison )* ;
comparison = addition ( ( "<" | ">" | "<=" | ">=" ) addition )* ;
addition = multiplication ( ( "+" | "-" ) multiplication )* ;
multiplication = unary ( ( "*" | "/" | "%" ) unary )* ;
unary = ( "!" | "-" ) unary | postfix ;
postfix = primary ( call | index | field_access | "?" )* ;
call = "(" arg_list? ")" ;
arg_list = expr ( "," expr )* ;
index = "[" expr "]" ;
field_access = "." IDENT ;
primary = INT | FLOAT | STRING | "true" | "false" | "null"
| IDENT
| "(" expr ")"
| array_lit
| object_lit
| lambda
| pipeline
| must_expr
| ask_expr
| freeze_expr
| spread_expr
| await_expr
| struct_init
| where_filter
| pipe_chain
| block_expr ;
array_lit = "[" ( expr ( "," expr )* ","? )? "]" ;
object_lit = "{" ( field_init ( "," field_init )* ","? )? "}" ;
field_init = ( IDENT | STRING ) ":" expr | IDENT ;
lambda = "|" param_list? "|" ( expr | block ) ;
pipeline = expr "|>" expr ;
must_expr = "must" expr ;
ask_expr = "ask" expr ;
freeze_expr = "freeze" expr ;
spread_expr = "..." expr ;
await_expr = ( "await" | "hold" ) expr ;
struct_init = IDENT "{" ( IDENT ":" expr ( "," IDENT ":" expr )* ","? )? "}" ;
where_filter = expr "where" IDENT comparison_op expr ;
pipe_chain = "from" expr ( "keep" expr | "sort" "by" IDENT | "take" expr )+ ;
block_expr = "{" statement* expr "}" ;
Types
type_ann = simple_type
| array_type
| generic_type
| function_type
| optional_type ;
simple_type = "Int" | "Float" | "String" | "Bool" | "Json" | IDENT ;
array_type = "[" type_ann "]" ;
generic_type = IDENT "<" type_ann ( "," type_ann )* ">" ;
function_type = "fn" "(" ( type_ann ( "," type_ann )* )? ")" "->" type_ann ;
optional_type = type_ann "?" ;
Lexical Elements
IDENT = ( ALPHA | "_" ) ( ALPHA | DIGIT | "_" )* ;
INT = DIGIT+ ;
FLOAT = DIGIT+ "." DIGIT+ ;
STRING = '"' ( CHAR | ESCAPE | INTERP )* '"' ;
RAW_STRING = '"""' CHAR* '"""' ;
INTERP = "{" expr "}" ;
ESCAPE = "\\" ( "n" | "t" | "r" | "\\" | '"' | "{" ) ;
NEWLINE = "\n" | "\r\n" | ";" ;
COMMENT = "//" CHAR* NEWLINE ;
Operator Summary
| Precedence | Operators | Associativity |
|---|---|---|
| 1 (lowest) | || | Left |
| 2 | && | Left |
| 3 | == != | Left |
| 4 | < > <= >= | Left |
| 5 | + - | Left |
| 6 | * / % | Left |
| 7 | ! - (unary) | Right |
| 8 | ? (postfix try) | Left |
| 9 (highest) | . [] () | Left |
Notes
- Newlines are significant. They terminate statements unless the line ends with an operator or open delimiter.
- Semicolons can be used as explicit statement terminators.
- Comments start with
//and extend to the end of the line. - String interpolation uses
{expr}inside double-quoted strings. Raw strings ("""...""") do not interpolate. - The
haskeyword is not reserved. It is parsed contextually inside struct/thing bodies.
Keywords
Alphabetical list of all reserved keywords in the Forge language. Keywords cannot be used as identifiers.
Keyword Table
| Keyword | Category | Description |
|---|---|---|
any | Innovation | Existential check over collection |
ask | Innovation | AI/LLM prompt call |
async | Classic | Async function modifier |
await | Classic | Await an async expression |
break | Control flow | Exit the current loop |
by | Innovation | Sort/order modifier (sort by, order by) |
catch | Error handling | Catch block in try/catch |
change | Natural | Reassign a variable (change x to 10) |
check | Innovation | Declarative validation |
continue | Control flow | Skip to next loop iteration |
craft | Natural type | Constructor call (craft Person { }) |
crawl | Innovation | Web scraping |
define | Natural | Function definition (alias for fn) |
download | Innovation | Download a file (download url to path) |
each | Natural | Iterator keyword (for each x in items) |
else | Control flow | Else branch in if/else |
emit | Natural | Yield a value (alias for yield) |
every | Innovation | Interval modifier (schedule every 5 seconds) |
false | Literal | Boolean false |
fn | Classic | Function definition |
for | Control flow | For loop |
forge | Natural | Async function modifier (alias for async fn) |
freeze | Innovation | Make a value immutable |
from | Natural | Source keyword (grab x from url, from x import y) |
give | Natural type | Impl block (alias for impl) |
grab | Natural | Fetch from URL (grab resp from "url") |
hold | Natural | Await expression (alias for await) |
if | Control flow | Conditional branch |
impl | Classic | Implementation block |
import | Module | Import from a module |
in | Control flow | Iterator membership (for x in items) |
interface | Classic | Interface definition |
keep | Innovation | Filter in pipe chain |
let | Classic | Variable declaration |
limit | Innovation | Limit results in query pipeline |
loop | Control flow | Infinite loop |
match | Control flow | Pattern matching |
must | Innovation | Crash on error with clear message |
mut | Classic | Mutable modifier |
nah | Natural | Else branch (alias for else) |
null | Literal | Null value |
order | Innovation | Order results in query pipeline |
otherwise | Natural | Else branch (alias for else) |
power | Natural type | Interface definition (alias for interface) |
prompt | Innovation | Prompt template definition |
pub | Visibility | Public visibility modifier |
repeat | Natural | Counted loop (repeat 5 times { }) |
retry | Innovation | Automatic retry (retry 3 times { }) |
return | Control flow | Return from function |
safe | Innovation | Null-safe execution block |
say | Natural | Print with newline (alias for println) |
schedule | Innovation | Cron-style scheduling |
seconds | Natural | Time unit for wait and timeout |
select | Innovation | Select fields in query pipeline |
set | Natural | Variable declaration (alias for let) |
spawn | Concurrency | Spawn a concurrent task |
struct | Classic | Struct definition |
table | Innovation | Tabular data display |
take | Innovation | Take N items in pipe chain |
the | Natural type | Connector (give X the power Y) |
thing | Natural type | Struct definition (alias for struct) |
timeout | Innovation | Time-limited execution block |
times | Natural | Loop count modifier (repeat 5 times) |
to | Natural | Assignment target (set x to 5, download url to path) |
transform | Innovation | Data transformation |
true | Literal | Boolean true |
try | Error handling | Try block in try/catch |
type | Classic | Algebraic data type definition |
unless | Innovation | Postfix conditional negation |
unpack | Natural | Destructuring (alias for let { }) |
until | Innovation | Postfix loop termination |
wait | Natural | Sleep with time units (wait 2 seconds) |
watch | Innovation | File change detection block |
when | Innovation | Guard-based conditional |
where | Innovation | Collection filter |
while | Control flow | While loop |
whisper | Natural | Print in lowercase |
yield | Classic | Yield a value from a generator |
yell | Natural | Print in uppercase |
Built-in Type Names
These identifiers are recognized as type annotations. They are reserved in type position but can be used as regular identifiers in value position.
| Name | Type |
|---|---|
Int | 64-bit integer |
Float | 64-bit float |
String | UTF-8 string |
Bool | Boolean |
Json | JSON value |
Categories
| Category | Count | Keywords |
|---|---|---|
| Classic | 10 | async, await, fn, impl, interface, let, mut, struct, type, yield |
| Control flow | 12 | break, continue, else, for, if, in, loop, match, return, while, each, from |
| Natural | 13 | change, define, emit, forge, grab, hold, nah, otherwise, say, set, to, unpack, whisper, yell |
| Natural type | 5 | craft, give, power, the, thing |
| Innovation | 21 | any, ask, by, check, crawl, download, every, freeze, keep, limit, must, order, prompt, retry, safe, schedule, select, table, take, timeout, transform, unless, until, watch, when, where |
| Error handling | 2 | catch, try |
| Concurrency | 1 | spawn |
| Literal | 3 | false, null, true |
| Natural time | 2 | repeat, seconds, times, wait |
| Visibility | 1 | pub |
| Module | 1 | import |
Non-Keywords
The following identifiers are not reserved keywords. They are built-in functions or contextual identifiers that can be shadowed:
has– parsed contextually inside struct/thing bodiesprint,println– built-in functions, not keywordsOk,Err,Some,None– built-in constructorsself– not reserved; methods receiveselfas a regular parameter name- Module names (
math,fs,io, etc.) – pre-loaded global variables, not keywords
Operator Precedence
Operators listed from lowest precedence (evaluated last) to highest precedence (evaluated first). Operators at the same precedence level are evaluated according to their associativity.
Precedence Table
| Level | Operator | Description | Associativity |
|---|---|---|---|
| 1 | || | Logical OR | Left |
| 2 | && | Logical AND | Left |
| 3 | == != | Equality | Left |
| 4 | < > <= >= | Comparison | Left |
| 5 | + - | Addition, subtraction | Left |
| 6 | * / % | Multiply, divide, modulo | Left |
| 7 | ! - (unary) | Logical NOT, negation | Right (unary) |
| 8 | ? | Postfix try (Result) | Left |
| 9 | . [] () | Access, index, call | Left |
Special Operators
These operators do not fit neatly into the arithmetic precedence chain.
Pipe Operator |>
let result = data |> transform |> validate
The pipe operator has lower precedence than function calls but higher than assignment. It passes the left-hand value as the first argument to the right-hand function.
Pipe Right >>
from users >> keep where active >> sort by name >> take 5
Used in query-style pipe chains. Evaluated left to right.
Spread ...
let merged = [...arr1, ...arr2]
let combined = { ...obj1, ...obj2 }
Prefix operator used inside array and object literals. Not a general expression operator.
Range ..
let r = 1..10
Creates a range value. Used primarily in for loops and slice operations.
Arrow ->
match x {
1 -> "one",
_ -> "other",
}
Used in match arms and when arms to separate pattern from result. Not a general operator.
Fat Arrow =>
let f = (x) => x * 2
Lambda shorthand syntax. Separates parameters from body.
Compound Assignment
| Operator | Equivalent |
|---|---|
+= | x = x + value |
-= | x = x - value |
*= | x = x * value |
/= | x = x / value |
Compound assignment operators have the same precedence as regular assignment (=). They are statement-level constructs, not expressions.
Type Operators
| Operator | Context | Description |
|---|---|---|
: | let x: Int = 5 | Type annotation |
? | fn f(x: Int?) { } | Optional type modifier |
<> | Array<Int> | Generic type parameters |
Type operators appear only in type annotation positions and do not participate in expression evaluation.
Examples
// Precedence determines evaluation order
let x = 2 + 3 * 4 // 14 (not 20)
let y = !true || false // false (! binds tighter than ||)
let z = 1 < 2 && 3 > 1 // true (&& binds looser than < and >)
// Postfix try with field access
let name = get_user()?.name // ? applies to get_user(), then .name
// Pipe with arithmetic
let result = 5 + 3 |> double // double(8), not 5 + double(3)
Gotchas
- Unary
-binds tighter than binary operators:-2 * 3is(-2) * 3 = -6, not-(2 * 3) = -6(same result in this case, but matters for method calls). - The
?operator binds tighter than., soexpr?.fieldworks as expected: it triesexpr, then accesses.fieldon the result. - There is no ternary
? :operator. Useif/elseexpressions orwhenguards instead. ==and!=compare by value for all types. There is no identity comparison operator.
Changelog
v0.3.3
Type system – natural syntax
- Added
thingkeyword as alias forstruct - Added
powerkeyword as alias forinterface - Added
givekeyword as alias forimpl - Added
give X the power Ysyntax for interface implementation - Added
craftkeyword for struct construction - Added
hascontextual parsing inside struct/thing bodies (not a reserved keyword) - Interface satisfaction checking (Go-style structural typing)
v0.3.2
Landing page and book
- Added mdBook-based language specification
- Created project landing page
- Documentation improvements across all modules
v0.3.1
Bug fixes and stabilization
- Fixed production gaps identified by architecture audit
- Comprehensive integration test suite (179 tests)
- Stability improvements for interpreter, VM, and JIT tiers
v0.3.0
Standard library expansion
- Expanded stdlib to 16 modules with 230+ functions
- Added
timemodule (25 functions: now, unix, parse, format, diff, add, sub, zone, etc.) - Added
npcmodule (16 fake data generators) - Added
csvmodule (parse, stringify, read, write with auto type inference) - Added
termmodule (25+ functions: colors, table, sparkline, bar, banner, box, gradient, menu, etc.) - Added
execmodule (run_command with stdout/stderr/status) - Expanded
httpmodule with download and crawl - Expanded
fsmodule to 20 functions - Native
Option<T>values (Some,None,is_some,is_none) - Tokio
spawnwith language-level task handles - Language-level channels (
channel,send,receive,try_send,try_receive) - GenZ debug kit (
sus,bruh,bet,no_cap,ick) - Execution helpers (
cook,yolo,ghost,slay) - 30 interactive tutorials via
forge learn - Shell integration (
sh,shell,sh_lines,sh_json,sh_ok,pipe_to) - Dual syntax (natural language keywords alongside classic syntax)
- Innovation keywords:
when,must,safe,check,retry,timeout,schedule,watch,ask,download,crawl,repeat,wait,grab,emit,hold - Pipe operator (
|>) and query-style pipe chains wherefilter syntaxfreezefor immutable values- Decorator system (
@server,@get,@post,@put,@delete,@ws) - HTTP server powered by axum + tokio
- WebSocket support
- PostgreSQL support via
pgmodule - Compound assignment operators (
+=,-=,*=,/=) - Spread operator (
...) in arrays and objects - String interpolation (
"hello, {name}") - Raw strings (
"""no interpolation""") - Destructuring (
let {a, b} = obj/unpack {a, b} from obj) - Default parameter values
- 12 example programs
- 13 CLI commands (run, repl, version, fmt, test, new, build, install, lsp, learn, chat, help, -e)
v0.2.0
VM and JIT tiers
- Register-based bytecode VM (
--vmflag) - 32-bit instruction encoding (ABC, ABx, AsBx formats)
- Mark-sweep garbage collector for VM heap
- Green thread scheduler in VM
- Cranelift JIT compiler (
--jitflag) - JIT achieves ~11x faster than Python on
fib(30) - Bytecode compiler from AST to VM instructions
- VM supports core language features (variables, functions, closures, control flow, arrays, objects)
- JIT supports integer/float arithmetic, function calls, recursion, basic control flow
v0.1.0
Initial release
- Tree-walking interpreter in Rust
- Core language: variables, functions, closures, if/else, for, while, match
- Basic type system: Int, Float, Bool, String, Array, Object, Null
- Result type (
Ok,Err) with?operator math,fs,io,crypto,db,env,json,regex,logmodules- Built-in HTTP client via
reqwest - SQLite database access via
rusqlite - REPL mode
- Formatter (
forge fmt) - Test runner (
forge test) - Project scaffolding (
forge new)