Repository navigation
Language Syntax
- Comments
- Tokens
- List & Map Literals and Comprehensions
- Set Literals and Comprehensions
- Functions
- Arrow Functions
- Spread & Rest
- Pipe Operator
- Classes
- Structs
- Enums
- Generators (
yield) - Async / Await
- Operators & Punctuation
- Precedence (high to low)
switchStatementdeferStatementmatchExpressiontry/catch/finallyStatementsexportStatementsimportStatementsconstBindings- Destructuring
let
CupidScript uses a C-like surface syntax.
Supported comment forms:
// line comment
/* block comment */Comments are skipped by the lexer alongside whitespace.
- Start: letter or
_ - Continue: letters, digits,
_
Decimal and hexadecimal integers with optional underscores:
0
12345
1_000_000
0xFF
0xFF_FF_FF- Decimal:
0-9digits - Hexadecimal:
0xor0Xfollowed by hex digits (0-9,a-f,A-F) - Underscores
_can appear anywhere in the number (ignored)
Floating-point literals with decimal point:
3.14
2.5
0.001
1.5e-3
2.0E+10- Must contain a decimal point:
. - Optional exponent:
eorEfollowed by optional+/-and digits - Scientific notation:
1.5e-3equals0.0015
Double-quoted strings:
"hello"
"with \n escapes"
"quote: \""
"backslash: \\""Raw (backtick) strings:
`C:\\temp\\file`
`line1
line2`Notes:
- Backtick strings do not process escapes.
- Backtick strings can span multiple lines.
- String interpolation is not supported inside backtick strings.
- A backtick cannot appear inside a backtick string.
String interpolation is supported with ${...} inside a string:
let name = "Ada";
print("Hello ${name}");
print("sum=${1 + 2}");Escape handling (in runtime unescape):
-
\n,\t,\r,\",\\ -
\e(ASCII ESC, 27) -
\xHH(single byte from two hex digits) - Any unknown escape
\xbecomes literalx
[]
[1, 2, 3]
["a", "b", "c"]Trailing commas are allowed:
[1, 2, 3,]
[]List Comprehensions:
[x * x for x in range(10)]
[x for x in items if x > 0]See Comprehensions for details.
{}
{"name": "Frank", "age": 30}
{"key": value_expr}Trailing commas are allowed:
{"a": 1, "b": 2,}
{}Map Comprehensions:
{k: v * 2 for k, v in data}
{i: i * i for i in range(5)}See Comprehensions for details.
Keys are expressions (any value is allowed). For convenience, an identifier before : is treated as a string key.
#{}
#{1, 2, 3}
#{"apple", "banana", "cherry"}Trailing commas are allowed:
#{1, 2, 3,}
#{}Set Comprehensions:
#{x * x for x in range(10)}
#{x for x in items if x > 0}Spread in Set Literals:
let a = #{1, 2, 3};
let b = #{0, ...a, 4}; // #{0, 1, 2, 3, 4}
let list = [1, 2, 2, 3];
let from_list = #{...list}; // #{1, 2, 3}Set Operators:
let a = #{1, 2, 3};
let b = #{2, 3, 4};
a | b // union: #{1, 2, 3, 4}
a & b // intersection: #{2, 3}
a - b // difference: #{1}
a ^ b // symmetric difference: #{1, 4}See Collections for complete set documentation.
Named functions:
fn add(a, b = 1) {
return a + b;
}Function literals:
let inc = fn(x, step = 1) { return x + step; };Defaults use = in the parameter list. Required parameters must come before default parameters. Missing arguments use their default values.
Arrow syntax supports concise single-expression functions:
let double = fn(x) => x * 2;
let add = fn(a, b) => a + b;Block-bodied arrows require explicit return:
let f = fn(x) => { return x + 1; };Spread (...) expands lists and maps (can appear at any position, including first):
let a = [1, 2, 3];
let b = [0, ...a, 4]; // [0, 1, 2, 3, 4]
let c = [...a, 5]; // [1, 2, 3, 5] - spread at first position
let defaults = {theme: "dark", size: 12};
let config = {...defaults, size: 14}; // {theme: "dark", size: 14}
let merged = {...defaults, ...config}; // spread at first positionRest parameters collect remaining arguments into a list:
fn log_all(prefix, ...items) {
for item in items { print(prefix, item); }
}The pipe operator passes the left value into the right call.
fn add(a, b) { return a + b; }
let r1 = 10 |> add(5); // add(10, 5)
let r2 = 10 |> add(5, _); // add(5, 10)If _ is present, it marks where the left value should go. Otherwise, the left value becomes the first argument.
Class declarations define constructors and methods with single inheritance.
class File {
fn new(path) { self.path = path; }
fn is_hidden() { return starts_with(self.path, "."); }
}
class ImageFile : File {
fn new(path) { super.new(path); self.is_img = ends_with(path, ".png"); }
fn is_image() { return self.is_img; }
}
let f = File(".secret");
let img = ImageFile("cat.png");-
selfrefers to the instance inside methods. -
superrefers to the parent class inside methods. - Instances are created by calling the class as a function.
Structs define fixed-field data types with positional construction.
struct Point { x, y = 0 }
let p = Point(3); // x=3, y=0
let q = Point(1, 2); // x=1, y=2Enums define named integer constants:
enum Color { Red, Green = 5, Blue }
print(Color.Red); // 0
print(Color.Green); // 5
print(Color.Blue); // 6Functions that use yield return a list of yielded values when called:
fn range(n) {
let i = 0;
while (i < n) {
yield i;
i += 1;
}
}
let xs = range(4); // [0, 1, 2, 3]Async functions and await are supported (currently synchronous execution):
async fn add(a, b) { return a + b; }
let v = await add(2, 3);( ) [ ] { } , ; .
Assignment:
=-
+=,-=,*=,/=(compound assignment, works on variables, fields, and index expressions) -
:=(walrus operator - assigns and returns value in expressions)
Arithmetic:
+ - * / %
Unary:
! -
Comparison:
== != < <= > >=
Boolean:
&& ||
Bitwise / Set Operations:
-
&(bitwise AND / set intersection) -
|(bitwise OR / set union) -
^(bitwise XOR / set symmetric difference)
Note: These operators work on sets when both operands are sets, otherwise they are reserved for future bitwise operations on integers.
Nullish coalescing:
-
??(returns left operand if notnil, otherwise right)
Range:
-
..(exclusive range, e.g.,0..5→[0,1,2,3,4]) -
..=(inclusive range, e.g.,0..=5→[0,1,2,3,4,5])
The walrus operator (:=) assigns a value to a variable and returns that value, allowing assignment within expressions:
// Assignment in conditionals
if (x := get_value()) {
print("Got value:", x); // x is available here
}
// Assignment in while loops
while (line := read_line()) {
process(line);
}
// Assignment in function arguments
result := process(data := fetch_data());
// Chain assignments
print(y := (x := 10) + 5); // x=10, y=15The walrus operator is useful for:
- Avoiding redundant function calls: Assign and test in one expression
- Cleaner conditionals: Combine assignment with condition checking
- Loop conditions: Assign and check loop variables simultaneously
Rules:
- Left side must be a simple identifier (not a field or index expression)
- Returns the assigned value
- Can be used anywhere an expression is valid
- Precedence is lower than most operators but higher than ternary
Example patterns:
// Read-process-check pattern
while (data := fetch_next()) {
if (result := process(data)) {
save(result);
}
}
// Avoid double computation
if (value := expensive_computation()) {
use(value); // value already computed and stored
}
// Nested conditions
if (user := find_user(id)) {
if (perms := get_permissions(user)) {
check_access(perms);
}
}- Primary: literals, identifiers, parenthesized
(expr), list literals[...], map literals{...}, set literals#{...} - Postfix: calls
f(...), indexinga[b], field accessa.b(field access works after any expression, e.g.(expr).field) - Unary:
!expr,-expr - Multiplicative:
* / % - Additive:
+ - - Range:
start..end,start..=end - Comparison:
< <= > >= - Equality:
== != - Bitwise AND / Set Intersection:
& - Bitwise XOR / Set Symmetric Difference:
^ - Bitwise OR / Set Union:
| - Logical AND:
&& - Logical OR:
|| - Nullish:
a ?? b - Pipe:
value |> f() - Ternary:
cond ? then : else - Python-style conditional (in comprehensions):
value_if_true if cond else value_if_false
Note: The Python-style
if-elsesyntax (#17) is primarily supported in comprehension expressions. For general use, prefer the traditional ternary operator (#16).
Optional chaining accesses a field only if the target is not nil:
user?.nameIf the target is nil, the result is nil (no error). Otherwise, it behaves like normal field access.
CupidScript supports switch as a statement:
switch (x) {
case 1 { print("one"); }
case 2 { print("two"); }
default { print("other"); }
}Notes:
- Cases are compared using the same equality rules as
==for basic types. - Fallthrough: once a case matches, execution continues into subsequent cases until a
breakis hit or the switch ends. -
defaultcan appear anywhere; if no case matches, execution starts atdefaultand falls through to later cases.
Pattern cases are supported (no guards):
switch (value) {
case [a, b, ...rest] { print(a, b, rest); }
case {x, y: yy} { print(x + yy); }
case int(x) { print("int", x); }
default { print("other"); }
}Schedule cleanup work to run when the current block exits:
defer close(file);
defer { print("cleanup"); }Notes:
- Defers run in LIFO order when the block exits.
- Defers run even if the block exits via
return,break,continue, orthrow.
match is like switch, but returns a value and is used inside expressions.
let label = match (x) {
case 1: "one";
case 2: "two";
default: "other";
};Patterns and guards:
let label = match (value) {
case [a, b]: a + b;
case {x, y: z}: x + z;
case n if n > 10: "big";
case _: "small";
};Notes:
- Patterns can bind identifiers; bindings are visible in the guard and case result.
- Guards (
if expr) are optional and run only when the pattern matches. - The first matching case expression is evaluated and returned.
-
defaultis optional; if omitted and no case matches, the result isnil.
Syntax:
try {
// body
} catch (e) {
// handler
} finally {
// cleanup
}
// finally can also be a single expression:
try { work(); } catch (e) { print(e); } finally cleanup();Notes:
-
catchis required;finallyis optional. -
finallyexecutes after thetry/catchregardless ofreturnorthrow. -
finallycan be a block or a single expression statement.
Syntax:
export name = expr;
export {name, local as exported};Defines a value in the module's implicit exports map. require() returns this map.
Syntax:
import "./path/to/module.cs"; // side effects only
import mod from "./path/to/module.cs"; // bind exports map
import {foo, bar as baz} from "./path/to/module.cs";
import mod, {foo} from "./path/to/module.cs";Notes:
-
importis syntax sugar overrequire(). - Named imports read fields from the module's exports map.
- If an imported name is missing, the binding is set to
nil.
Use const to create an immutable binding:
const x = 10;Notes:
- Reassignment to a
constbinding raises a runtime error. -
constrequires an initializer. - Destructuring also works with
const:
const [a, b] = [1, 2];
const {name, age} = {"name": "Ada", "age": 36};List and map destructuring are supported in let declarations.
let [a, b, c] = [1, 2];
// a=1, b=2, c=nillet {x, y} = {"x": 10, "y": 20};
// x=10, y=20
let {key: alias} = {"key": "value"};
// alias="value"Notes:
- Missing list elements or map keys produce
nil. -
_can be used to ignore a binding. - Destructuring requires an initializer.
- List destructuring expects a list; map destructuring expects a map (runtime error otherwise).