CuffScript Guide
This guide covers the whole CuffScript syntax, plus what is worth knowing when you run code in this browser IDE. Use the table of contents on the left to jump to any topic.
What is CuffScript
CuffScript is an interpreted language that expresses variables, functions and control flow with keywords close to natural language (set, to, do:, end). Like Python, it uses indentation to separate blocks. Lists and strings are indexed from 1, and regular expressions are written with readable tokens such as [num] and [str] instead of symbols like \d and \w.
Using this IDE
- Run (Ctrl/Cmd + Enter): runs the currently open tab as the entry point.
- View AST: shows the tokens and syntax tree without running the code. It is the same information as
cuffc --ast. - Standard input (stdin):
input()reads the text you pre-fill in the stdin tab, one line at a time from the top, and returns an empty string once it runs out. (In environments that support live input, a field appears in the output panel instead.) - File tabs: the
+button adds files, so you can build multi-file projects that load each other withuse ... from .... A run always starts from the active tab. - Share link: copies a URL that contains the whole project. No server storage is involved, so you can pass code around with just a link.
- Download: saves the current tab as a
.cufffile. - Your work is saved automatically in the browser's localStorage, so it survives a refresh.
Variables and constants
The first time you create a value, always write set [type] [name] to [value]. To change it afterwards, use change [name] to [value].
set number age to 25
set str name to "Alice"
set empty data to empty
change age to 26
change name to "Bob"
Constants are declared with set constant [type] [name] to [value]. The name must be all upper case (UPPER_CASE), and trying to change it later is a runtime error.
set constant number MAX_RETRIES to 3
set constant str API_URL to "https://cufflang.dev"
constant also works with list and map. A constant list is fully read-only, like a Python tuple: adding, removing and index assignment are all runtime errors, and it stays read-only when you assign it to another variable or pass it to a function. The protection is shallow, though: a list or map stored inside a constant list can still be changed. A constant map refuses change CONFIG["key"] to ... and remove "key" from CONFIG, but the map itself is not frozen: another variable that was given the same map can still change it, and the constant then shows that change. Copy it with map_merge(CONFIG, {}) when you need an independent map.
set constant list PRIMES to [2, 3, 5, 7, 11]
add 13 to PRIMES note: runtime error, a frozen list can't be added to
change PRIMES[1] to 0 note: also a runtime error
set constant map SETTINGS to {"volume": 5}
change SETTINGS["volume"] to 9 note: runtime error, a constant map can't be changed through its name
Words such as add, count, find, split, replace, match, in, by, not and global can also be used as the names of variables, functions, parameters and loop variables (set number count to 3). A word that has its own syntax, like find, is treated as syntax only when that syntax follows it, and as an ordinary name otherwise. The exception is and and or: they are operators, so they can't be used as names.
Data types
| Type | Description | Example |
|---|---|---|
number | Integer or decimal | 5, 3.14 |
str | String | "hello" |
boolean | True or false | true, false |
list | 1-based ordered list | ["a", "b"] |
map | Dictionary with string keys | {"k": "v"} |
empty | No value | empty |
Function parameters can be written without a type, and you can add one if you like (Defining functions). An f-string (f"...") embeds expressions in a string, and to print a brace itself you write it twice, as {{ }}.
set number x to 5
print(f"x + 1 = {x + 1}")
print(f"JSON-like: {{\"key\": \"value\"}}")
The colon rule and comments
To enforce readability at the language level, every colon (:) outside a string follows one rule: never a space before it, a space after it is recommended. Breaking it is a syntax error from the lexer right away. If the body after do: fits on one line, you can add end at the end of that line and write a one-line form with no indentation.
A single-line comment is note: text. A multi-line comment runs from the line after note: up to just before endnote.
note: a single-line comment
set number x to 10
if x is 10 do: print("Passed") end note: a trailing comment on the same line works too
note:
This is a multi-line comment area.
Indentation and line breaks are free-form.
endnote
Passed
Operators
is is a normal equality check, and IS is a comparison that ignores English letter case. ! flips a boolean.
set str input_text to "Apple"
if input_text is "apple" do: print("Not run: the case differs") end
if input_text IS "apple" do: print("Runs because IS ignores case") end
set boolean is_active to false
if !is_active do: print("Runs because ! flips the value") end
Combine conditions with and and or (&& and || mean the same). The operands can be of any type, and the result is always a boolean. When the left side alone decides the result, the right side is not evaluated, so x is not 0 and 10 / x > 1 never divides by zero.
set number age to 30
if age >= 18 and age < 65 do: print("Working age") end
print(age < 18 or age >= 65)
set number x to 0
print(x is not 0 and 10 / x > 1)
Working age
false
false
% is the remainder of a division, and its sign follows the divisor (-7 % 3 is 2). ** is exponentiation and groups from the right (2 ** 3 ** 2 is 512, and -2 ** 2 is -4). Both are the same calculations as math_mod and math_pow, and a remainder by zero is a DivisionByZero (E4-006) error.
print(7 % 3, -7 % 3, 2 ** 10, 2 ** 3 ** 2, -2 ** 2)
1 2 1024 512 -4
The binding strength weakens in this order: **, unary -, * / %, + -, comparisons (including is), !, and, or. That is why !x is 3 means !(x is 3).
Indexing and slicing
The first position of a list or string is 1. Index 0 does not exist, and accessing it is a runtime error. Negative indexes count from the end, so the last item is -1. Slices use a tilde, as in [start~end], and include both ends.
set list colors to ["red", "green", "blue", "yellow"]
print(colors[1]) note: "red"
print(colors[-1]) note: "yellow"
print(colors[2~3]) note: ["green", "blue"]
Conditionals and loops
Branching looks like if ... do: ... else if ... do: ... else do: ... end. There are two loops: loop repeat, which runs over a range, and loop while, which runs while a condition is true. stop ends only the nearest loop immediately. Block statements must be indented, and every opener needs exactly one end.
set number score to 85
if score >= 90 do:
print("Excellent")
else if score >= 80 do:
print("Good")
else do:
print("Keep trying")
end
loop repeat i to 1 ~ 10 do:
if i is 4 do:
stop
end
print(f"Round: {i}")
end
Good
Round: 1
Round: 2
Round: 3
loop repeat always runs over a number range, and both ends are included. If the start is larger than the end it counts down, and a range is never empty: 1 ~ 0 runs twice, for 1 and then 0. Take care when the end comes from the length of a list that might be empty, since 1 ~ length(items) then still runs twice.
loop repeat i to 1 ~ 0 do:
print(i)
end
1
0
Working with lists and maps
Collections are handled with natural-language statements instead of methods: add ... to ... appends to a list, change ... to ... changes an index or key, and remove ... from ... removes. Assigning to a key that a map doesn't have creates a new key.
set list inventory to ["sword", "shield"]
add "potion" to inventory
change inventory[1] to "magic_staff"
remove 2 from inventory
set map profile to {"name": "Bob"}
change profile["level"] to 50
remove "level" from profile
Defining functions
Functions also start with set. The one-line shorthand is not allowed: a function is always written as an indented block. To return a value, add returnable and use return. Calls use only parentheses (), with no do:.
set returnable func fib(n) do:
if n <= 1 do:
return n
end
return fib(n - 1) + fib(n - 2)
end
print(f"fib(10) = {fib(10)}")
Nested function definitions and closures are not supported. Using set func inside a function raises NestedFunctionNotSupported (E4-018), so always define functions at the top level. A function can see only its own local variables; global variables have to be connected as described in Scope and global.
Put one of number, str, boolean, list, map or match before a parameter and the argument's type is checked when the function is called. Typed and untyped parameters can be mixed, and empty is accepted for any type. A wrong type raises ParameterTypeMismatch (E4-030) at the line of the call.
set func add_num(number n1, number n2) do:
print(n1 + n2)
end
add_num(1, 2)
add_num("1", 2) note: error at the call line, a string is not a number
The three modifiers returnable, async and pure can be combined freely, in any order. A pure function can neither read nor write global variables. Even the change name to global bridge is no exception: trying it raises a PureFunctionGlobalAccess (E4-027) runtime error right away. This restriction propagates through calls: a pure function can't call a user function that isn't pure either, and doing so raises PureFunctionImpureCall (E4-029) immediately. The function being called must be marked pure too. (Native functions such as print and DLC functions are not affected.)
set number shared_state to 0
set pure returnable func double(x) do:
return x * 2
end
set pure returnable func quad(x) do:
return double(double(x)) note: a pure function calling a pure one is fine
end
print(quad(5)) note: 20
set func bump() do:
change shared_state to global
change shared_state to shared_state + 1
end
set pure func broken() do:
bump() note: PureFunctionImpureCall, a pure function calls the non-pure bump()
end
Async (async / await)
Calling an async function without await does not run it immediately. It is queued, and runs after all the synchronous top-level code has finished, in the order it was queued (FIFO). Adding await runs it right now and gives you its result, if it has one.
set async func notify() do:
print("[async] done")
end
set async returnable func fetch_score() do:
return 87
end
print("[sync] start")
notify() note: queued, not run now
set number score to await fetch_score() note: await runs it immediately
print(f"[sync] score = {score}")
print("[sync] end")
note: the queued notify() runs after this point.
Scope and global
A function sees only the variables it creates itself with set (and its parameters). To use a global variable, whether to read it or to change it, first connect it with change [name] to global. Without that connection the name is an UndefinedVariable (E4-001) error. Other functions and built-in functions such as print can be called without any connection.
set number counter to 0
set func increment() do:
change counter to global
change counter to counter + 1
end
increment()
increment()
print(f"counter = {counter}")
or_else
Instead of try-catch, you attach or_else do: ... end after a statement that might fail. Inside the block you can fill an existing variable with change or declare a new one.
set number a to 10
set number b to 0
set number result to a / b or_else do:
print("Division by zero failed, using the default value")
change result to -1
end
print(f"result = {result}")
The error code system
Every error has a unique code (for example E4-006) and belongs to one category. The number between E and - tells you which stage it came from.
| Code | Category | Recoverable with or_else? |
|---|---|---|
E1-xxx | Lexical (tokenizer) | No |
E2-xxx | Syntax (parser) | No |
E3-0xx | Regex Syntax (pattern compile) | No |
E3-1xx | Regex Runtime (step and time limits, etc.) | Yes |
E4-xxx | Runtime (interpreter) | Yes |
E5-xxx | Module (use/from) | Yes |
E6-xxx | Resource (execution budget exceeded) | No |
E9-xxx | Internal (engine bug) | No |
If the program itself is wrong (a syntax error, say), the error can't appear inside code that is already running, so or_else does not catch it. Only "correct code running into a bad situation" (division by zero, a missing file, and so on) can be recovered with or_else. E6 happens only when the CLI's --max-steps or --timeout execution budget is exceeded (both are off by default). It is a sign that the program has gone out of control, so or_else is deliberately designed not to catch it.
Full list of error codes (72)
| Code | Category | Name |
|---|---|---|
E1-001 | Lexical | UnexpectedCharacter |
E1-002 | Lexical | UnterminatedString |
E1-003 | Lexical | InvalidNumberLiteral |
E1-004 | Lexical | InconsistentIndentation |
E1-005 | Lexical | ColonSpaceBeforeNotAllowed |
E1-006 | Lexical | UnterminatedComment |
E1-007 | Lexical | InvalidAssignmentSymbol |
E2-001 | Syntax | UnexpectedToken |
E2-002 | Syntax | ExpectedToken |
E2-003 | Syntax | MalformedFunctionDecl |
E2-004 | Syntax | MalformedControlFlow |
E2-005 | Syntax | MalformedImport |
E2-006 | Syntax | UnbalancedBlock |
E2-007 | Syntax | InvalidAssignmentTarget |
E2-008 | Syntax | NestingTooDeep |
E2-009 | Syntax | SourceTooLarge |
E3-001 | Regex (syntax) | RegexUnclosedGroup |
E3-002 | Regex (syntax) | RegexUnclosedBracket |
E3-003 | Regex (syntax) | RegexInvalidColonSpacing |
E3-004 | Regex (syntax) | RegexInvalidQuantifierRange |
E3-005 | Regex (syntax) | RegexStackedQuantifier |
E3-006 | Regex (syntax) | RegexEmptyToken |
E3-007 | Regex (syntax) | RegexUnknownToken |
E3-008 | Regex (syntax) | RegexInvalidEscape |
E3-009 | Regex (syntax) | RegexDanglingQuantifier |
E3-010 | Regex (syntax) | RegexUnexpectedCharacter |
E3-011 | Regex (syntax) | RegexPatternTooComplex |
E3-101 | Regex (runtime) | RegexStepLimitExceeded |
E3-102 | Regex (runtime) | RegexTimeout |
E3-103 | Regex (runtime) | RegexRecursionLimitExceeded |
E4-001 | Runtime | UndefinedVariable |
E4-002 | Runtime | UndefinedFunction |
E4-003 | Runtime | ConstantReassignment |
E4-004 | Runtime | InvalidConstantName |
E4-005 | Runtime | TypeMismatch |
E4-006 | Runtime | DivisionByZero |
E4-007 | Runtime | ZeroIndexAccess |
E4-008 | Runtime | IndexOutOfRange |
E4-009 | Runtime | KeyNotFound |
E4-010 | Runtime | ArgumentCountMismatch |
E4-011 | Runtime | NotCallable |
E4-012 | Runtime | InvalidOperand |
E4-013 | Runtime | LocalVariableOutOfScope |
E4-014 | Runtime | InvalidCollectionOperation |
E4-015 | Runtime | ElementNotFound |
E4-016 | Runtime | UnsupportedOperation |
E4-017 | Runtime | StackOverflow |
E4-018 | Runtime | NestedFunctionNotSupported |
E4-019 | Runtime | AwaitOnNonAsync |
E4-020 | Runtime | DeclarationTypeMismatch |
E4-021 | Runtime | InvalidGlobalDeclaration |
E4-022 | Runtime | ReturnOutsideFunction |
E4-023 | Runtime | StopOutsideLoop |
E4-024 | Runtime | FractionalIndex |
E4-025 | Runtime | InvalidArgumentValue |
E4-026 | Runtime | SizeLimitExceeded |
E4-027 | Runtime | PureFunctionGlobalAccess |
E4-028 | Runtime | NetworkRequestFailed |
E4-029 | Runtime | PureFunctionImpureCall |
E4-030 | Runtime | ParameterTypeMismatch |
E5-001 | Module | ModuleNotFound |
E5-002 | Module | ModuleParseFailed |
E5-003 | Module | CircularImport |
E5-004 | Module | UnknownDLC |
E5-005 | Module | DLCFeatureUnavailable |
E5-006 | Module | ModuleAccessDenied |
E5-007 | Module | ModuleLimitExceeded |
E5-008 | Module | FilesystemAccessDenied |
E6-001 | Resource | ExecutionStepLimit |
E6-002 | Resource | ExecutionTimeout |
E6-003 | Resource | OutOfMemory |
E9-001 | Internal | InternalError |
A real error looks like this when you run the code below. The source line where the error happened is shown with it, and the ^ points at the exact column (columns count characters, and the ^ stays lined up even after wide or multi-byte characters such as CJK text or emoji).
set number a to 10
set number b to 0
print(a / b) note: there is no or_else, so the program stops here and prints the message below
ERROR: [E4-006] Runtime Error at line 3, column 9: division by zero
print(a / b)
^
A runtime error that isn't wrapped in or_else stops the program on the spot. In this IDE the output panel shows a message like the one above in red, and the run status changes to Error.
Pattern matching: basics and tokens
Instead of the traditional regex symbols such as \d, \w, ^ and $, CuffScript uses readable bracket tokens like [num]. Checking with is / IS verifies, by default, that the whole string matches.
| Token | Meaning |
|---|---|
[num] | One digit |
[let] | One English letter |
[low] / [up] | One lower-case / upper-case English letter |
[str] | One English letter or digit |
[word] | One English letter, digit or underscore (for identifiers) |
[sp] | One whitespace character |
[nl] | One line-break character (\n or \r; a Windows \r\n is two characters, so use [nl]2) |
[any] | Any one character |
[int] / [float] / [hex] | A signed integer / a decimal with digits on both sides of the point (3.14, but not 10, .5 or 1e5) / one hexadecimal character |
[email] / [phone] / [url] | Presets for an email address / a South Korean phone number / a URL |
[edge] | Word boundary |
[start] / [end] | Start / end of the string |
[one:a|b] | Choose one of the alternatives |
[abc] / [!abc] | A set of characters / a negated set |
N / + / * / ? / N~M | Exactly N / one or more / zero or more / zero or one / N to M |
(...) | Capture group (accessed 1-based) |
<name:...> | Named capture |
To match a symbol itself, escape it with \. That is needed for [, (, <, ], ), > and \, and for a ~ that isn't part of a repeat range such as 3~5. +, * and ? need it only after something they could repeat. |, ., :, !, { and } are ordinary characters, and | is not alternation outside [one:...]: "(a|b)" matches the text a|b. As everywhere in the language, a colon inside a pattern may not have a space before it ([one:a|b] works, [one :a|b] is a syntax error).
set str phone to "010-1234-5678"
set str filename to "photo.png"
if phone is "[num]3-[num]4-[num]4" do: print("Valid number") end
if filename is "[str]+\.[one:jpg|png|gif]" do: print("Image file") end
Valid number
Image file
A few more examples make the tokens feel familiar:
set str color to "#ff00aa"
set str sentence to "my cat sat on the mat"
set str code to "xyz"
note: hex color code ("#" followed by 6 hex characters)
if color is "#[hex]6" do: print("Valid color code") end
note: [edge] marks a word boundary, so "cat" matches but "category" would not
if find "[edge]cat[edge]" from sentence is not empty do: print("Cat mentioned") end
note: [!abc] means any character not in the set, here 3 non-vowels in a row
if code is "[!aeiou]3" do: print("3 letters with no vowels") end
Valid color code
Cat mentioned
3 letters with no vowels
([any]+)+, the engine limits the number of matching steps and the time spent. When a limit is exceeded, the run is stopped safely with a Regex Runtime Error.
match / find / replace / split / count
match pulls out capture-group values. It searches the text for the first place the pattern matches, the way find does, and gives empty when nothing matches.
set str serial to "SN-2026-998"
set match result to match serial from "SN-([num]4)-([num]+)"
if result is not empty do:
print(f"Year: {result[1]}") note: "2026"
end
note: named captures are looked up by key, like a map
set match res to match "2026-12-25" from "<year:[num]4>-<month:[num]2>-<day:[num]2>"
print(res["year"])
find searches for a part of the text. On its own it returns the first matching string; with the g flag it returns every match as a 1-based list. When nothing matches, a plain find gives empty and find ... g gives an empty list [].
set str article to "Ticket numbers: T-123, T-456"
set list tickets to find "T-[num]3" from article g
print(tickets[1])
T-123
replace swaps the parts that match a pattern for another string. Add g to replace every match.
set str phone_log to "Call log: 010-1234-5678"
set str masked to replace "[num]4-[num]4" in phone_log to "****-****"
print(masked)
Call log: 010-****-****
split divides a string around a pattern.
set list parts to split "apple, banana,cherry" by ",[sp]*"
count counts how many times a pattern appears. It gives 0 when there are none.
set str article to "Ticket numbers 12 and 34"
print(count "[num]+" in article)
2
Flags can follow find, match, count and replace: i (ignore case), g (all matches) and m (multi-line). To use several, write them together, like gi.
Quick reference
[Character tokens]
[num] [let] [low] [up] [str] [word] [sp] [nl] [any]
[Preset tokens]
[int] [float] [hex] [email] [phone] [url] [edge] [start] [end]
[Quantifiers]
N + * ? N~M N~ ~M (add ? after one to make it lazy)
[Choice and sets]
[one:a|b] [abc] [!abc]
[Groups]
(...) capture group, 1-based index
<name:...> named capture
[Commands]
is / IS match find (g) replace ... to ... (g) split ... by ... count ... in ...
Flags: i (ignore case) g (all matches) m (multi-line)
use ... from ...
To load another .cuff file in the same project, write use [name] from [relative path]. In this IDE you can create several file tabs and try this syntax as it is. Module statements must be written on a single line.
note: lib/greetings.cuff
set constant str DEFAULT_GREETING to "Hello"
set returnable func greet(name) do:
change DEFAULT_GREETING to global
return DEFAULT_GREETING + ", " + name + "!"
end
note: main.cuff
use greetings from ./lib
print(greet("CuffScript"))
print(DEFAULT_GREETING)
Built-in DLC list
Official built-in libraries are loaded with a single use DLC:name line, and several can be loaded on one line separated by commas, as in use DLC:math, DLC:string (each item needs its own DLC:). All of their functions are registered as global names, so you call them directly, without a DLC: prefix.
sqrt → math_sqrt, upper → str_upper, get → network_get: a library prefix tells you from the name alone which use line a function came from. The only exceptions are length / contains / index_of (provided by the string, list and map libraries, and deliberately behaving the same across them, although index_of comes from the string and list libraries only) and to_json / from_json / to_number / to_str / to_boolean (whose names already show their direction).
Full function list
Every built-in function registered in the engine. The introductions below cover only the representative ones, with examples.
| Library | Functions |
|---|---|
DLC:math | math_sqrt, math_abs, math_pow, math_round, math_floor, math_ceil, math_trunc, math_sign, math_clamp, math_mod, math_log, math_log10, math_log2, math_atan2, math_pi, math_e, math_exp, math_sin, math_cos, math_tan, math_asin, math_acos, math_atan, math_min, math_max |
DLC:string | str_upper, str_lower, length, contains, index_of, str_starts_with, str_ends_with, str_repeat, str_char_code, str_from_char_code, str_trim, str_trim_start, str_trim_end, str_pad_left, str_pad_right |
DLC:time | time_now, time_timestamp |
DLC:random | random_float, random_int, random_seed, random_choice, random_shuffle |
DLC:list | length, contains, index_of, list_sort, list_reverse, list_join, list_unique, list_sum, list_average, list_flatten, list_range |
DLC:map | length, contains, map_keys, map_values, map_has_key, map_entries, map_merge |
DLC:convert | to_number, to_str, to_boolean |
DLC:json | to_json, from_json |
DLC:network | network_get, network_post |
DLC:filesystemBlocked in this browser IDE | file_exist, file_size, file_read, file_readlines, file_write, file_add, file_remove |
Always available (no use needed): print, input, type_of, to_number, to_str, to_boolean.
DLC:math
use DLC:math
print(math_sqrt(16)) note: 4
print(math_abs(-5)) note: 5
print(math_pow(2, 10)) note: 1024
print(math_round(3.6)) note: 4
print(math_floor(3.9)) note: 3
print(math_ceil(3.1)) note: 4
print(math_min(4, 1, 9)) note: 1 (takes any number of arguments)
print(math_max(4, 1, 9)) note: 9
DLC:string
use DLC:string
print(str_upper("hello")) note: "HELLO"
print(str_lower("HELLO")) note: "hello"
print(str_trim(" padded ")) note: "padded"
print(length("hello")) note: 5, an exception to the prefix rule
print(contains("hello world", "world")) note: true, also unprefixed
print(str_starts_with("hello", "he")) note: true
print(str_ends_with("hello", "lo")) note: true
DLC:time / DLC:random
use DLC:time
use DLC:random
print(time_now()) note: Unix timestamp (seconds, a decimal); time_timestamp() is exactly the same function
set number roll to random_int(1, 6)
print(f"Dice roll: {roll}") note: an integer from 1 to 6, both ends included
print(random_float()) note: a decimal from 0.0 up to, but not including, 1.0; it used to be called plain random()
DLC:list
These follow a functional style: they return a new list and leave the original untouched. list_sort works only when the list is all numbers or all strings.
use DLC:list
use DLC:convert
set list scores to [5, 2, 8, 1, 9, 2]
print(list_sort(scores)) note: [1, 2, 2, 5, 8, 9], scores itself is unchanged
print(list_reverse(scores)) note: [2, 9, 1, 8, 2, 5]
print(list_unique(scores)) note: [5, 2, 8, 1, 9]
note: list_join() takes only a list of strings, so convert numbers with to_str() first
set list score_strs to []
loop repeat i to 1 ~ 5 do:
add to_str(scores[i]) to score_strs
end
print(list_join(score_strs, ", ")) note: "5, 2, 8, 1, 9"
DLC:map
A map has no built-in way to list its keys, so load this library when you need to iterate. map_keys and map_values return lists in the map's insertion order.
use DLC:map
set map profile to {"name": "Bob", "level": 12}
print(map_keys(profile)) note: ["name", "level"]
print(map_values(profile)) note: ["Bob", 12]
print(map_has_key(profile, "level")) note: true
note: loop repeat only ever runs over a number range (start ~ end), so go through
note: a list by index. map_entries() returns a list of [key, value] pairs.
set list pairs to map_entries(profile)
loop repeat i to 1 ~ length(pairs) do:
set list entry to pairs[i]
print(f"{entry[1]} = {entry[2]}")
end
note: map_merge() returns a new map; when keys overlap, the second argument wins
set map defaults to {"volume": 50, "difficulty": "normal"}
set map overrides to {"difficulty": "hard"}
print(map_merge(defaults, overrides)) note: {"volume": 50, "difficulty": "hard"}
DLC:convert
use DLC:convert
set number n to to_number("42.5")
print(n + 1) note: 43.5
print(to_str(123) + "!") note: "123!"
print(to_boolean("")) note: false (an empty string, 0 and empty are false; any other string is true)
to_number, to_str and to_boolean are actually core built-ins that are always available, so you can call them without use DLC:convert. The declaration itself is harmless (it just registers the same functions again); write it explicitly when you want the code to show where they come from.
DLC:json
use DLC:json
set map user to {"name": "Alice", "level": 7, "active": true}
set str packed to to_json(user)
print(packed) note: {"name":"Alice","level":7,"active":true}
print(to_json(user, 2)) note: pretty-printed with 2-space indentation
set map parsed to from_json(packed)
print(parsed["name"]) note: "Alice"
JSON's object / array / string / number / true / false / null map to CuffScript's map / list / str / number / boolean / empty. from_json follows RFC 8259 strictly, so common variants such as trailing commas, single quotes and unquoted keys are rejected with an InvalidArgumentValue (E4-025) error.
DLC:network
network_get(url) and network_post(url, body[, content_type]) are backed by a working HTTP/1.1 client and return a map shaped like {"status", "ok", "body"}. Only plain HTTP is supported, and https:// is not quietly downgraded to plain text: it is an error right away. A connection failure, DNS failure or timeout is a NetworkRequestFailed (E4-028) runtime error, which or_else can catch. Requests to loopback and private ranges (including cloud metadata addresses) are blocked by default to prevent SSRF. A host that runs untrusted code can turn the library off completely with CuffEngine::Options::networkEnabled = false (or cuffc --no-network).
use DLC:network
set map res to network_get("http://example.com") or_else do:
print("Request failed: the network may be blocked, or this IDE environment may be the limit")
change res to {"status": 0, "ok": false, "body": ""}
end
if res["ok"] do:
print(res["body"])
end
cuffc. The IDE on this page compiles CuffScript to WebAssembly and runs it inside a browser tab, and a browser doesn't let a script open arbitrary TCP sockets at all. So it is safest to assume that network_get and network_post calls end in a failure that or_else has to catch in this IDE. To see real network requests, run a locally built cuffc.
DLC:filesystem
file_exist, file_size, file_read, file_readlines, file_write, file_add and file_remove read and write real local files next to the script. Every path is confined to the same sandbox root that use ... from uses to find modules (the folder the script is in, or a --root set by the host), so an attempt to escape with an absolute path or ../ is rejected with FilesystemAccessDenied (E5-008) before anything is touched. Symbolic links that point outside the root are rejected too. Calling file_read or file_readlines on a file that doesn't exist gives empty instead of an error. file_remove deletes regular files only: a folder, even an empty one, is not removed, and it returns false.
use DLC:filesystem
set str path to "notes.txt"
print(file_exist(path)) note: false
print(file_write(path, "line one\nline two")) note: true
print(file_add(path, "\nline three")) note: true, appends
print(file_size(path)) note: number of bytes
print(file_readlines(path)) note: ["line one", "line two", "line three"]
print(file_read("no_such_file.txt")) note: empty, not an error
print(file_remove(path)) note: true
print(file_exist(path)) note: false
DLC:network, this is not "mostly fails" but a firm block. Instead of leaving each file operation as a failure for or_else to catch, this IDE refuses to run at all, with an immediate error, if use DLC:filesystem appears anywhere in the code. The sandbox "file system" on this page is a virtual store that holds the other tabs of this IDE project, so letting code read and write it would only be confusing. In a locally installed cuffc it works normally unless you pass --no-filesystem.
use DLC:list and use DLC:convert, and they work together as long as no function names collide. The DLC:list example above also loads DLC:convert for to_str.
Common mistakes
| Mistake | Why it's a problem | How to fix it |
|---|---|---|
list[0] | Indexes start at 1. 0 is not a valid position. | The first element is list[1]. |
if x is 5 do : | A space before a colon is a syntax error. | Attach the colon to the word before it, as in do:. |
set constant number retries to 3 | The constant name isn't all upper case. | Write it in capitals, like RETRIES. |
Changing a constant with change | A constant can't be changed after it is declared. | If the value needs to change, declare it without constant. |
Reading or change-ing a global variable directly inside a function | A function sees only its own local variables. | Connect it first with change name to global. (Scope and global) |
is "[num]+" fails on text that merely contains a number | is / IS checks for a whole-string match, not a part of the string. | To check only part of a string, use find. |
set number x to input() | input() always returns a str. | Convert it with DLC:convert, as in to_number(input()). |
Opening a block and forgetting end | Every block (if, loop, func, ...) needs one end per opener. | Use the indentation to check that each block is closed exactly once. |
Known limitations
- Standard input: unless live input is available,
input()consumes the text pre-filled in the stdin panel one line at a time. Once it is used up,input()returns an empty string. - Network: in the native
cuffc,DLC:networksends real plain-HTTP requests. In this browser IDE the browser sandbox blocks raw socket access, so requests mostly end in failure (see the DLC list above). - File system:
DLC:filesystemis available only in the nativecuffc. In this browser IDE, ause DLC:filesystemin any file of the project makes the run refuse to start (see the DLC list above). - Recursion depth: a function call depth over 1,000 stops the run safely with a
StackOverflow(E4-017) runtime error. - Execution time limit: to protect the tab from infinite loops, this IDE caps execution time (8 seconds by default; you can always stop a run yourself).
- Nested function definitions and closures are not supported by the language itself.
- Async is not concurrency:
asyncfunctions don't run in parallel on OS threads. It is cooperative scheduling: they run in order after the top-level synchronous code finishes.
Related links
Everything covered here is also available as 10 runnable examples in the IDE's Load an example menu. We recommend working through them in order, from the basics.