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 with use ... 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 .cuff file.
  • Your work is saved automatically in the browser's localStorage, so it survives a refresh.
Beware of infinite loops CuffScript code runs in a separate worker inside the browser. If a run takes too long (8 seconds by default) it is stopped automatically, and you can always stop it yourself with the Stop button.

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

TypeDescriptionExample
numberInteger or decimal5, 3.14
strString"hello"
booleanTrue or falsetrue, false
list1-based ordered list["a", "b"]
mapDictionary with string keys{"k": "v"}
emptyNo valueempty

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.

CodeCategoryRecoverable with or_else?
E1-xxxLexical (tokenizer)No
E2-xxxSyntax (parser)No
E3-0xxRegex Syntax (pattern compile)No
E3-1xxRegex Runtime (step and time limits, etc.)Yes
E4-xxxRuntime (interpreter)Yes
E5-xxxModule (use/from)Yes
E6-xxxResource (execution budget exceeded)No
E9-xxxInternal (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)
CodeCategoryName
E1-001LexicalUnexpectedCharacter
E1-002LexicalUnterminatedString
E1-003LexicalInvalidNumberLiteral
E1-004LexicalInconsistentIndentation
E1-005LexicalColonSpaceBeforeNotAllowed
E1-006LexicalUnterminatedComment
E1-007LexicalInvalidAssignmentSymbol
E2-001SyntaxUnexpectedToken
E2-002SyntaxExpectedToken
E2-003SyntaxMalformedFunctionDecl
E2-004SyntaxMalformedControlFlow
E2-005SyntaxMalformedImport
E2-006SyntaxUnbalancedBlock
E2-007SyntaxInvalidAssignmentTarget
E2-008SyntaxNestingTooDeep
E2-009SyntaxSourceTooLarge
E3-001Regex (syntax)RegexUnclosedGroup
E3-002Regex (syntax)RegexUnclosedBracket
E3-003Regex (syntax)RegexInvalidColonSpacing
E3-004Regex (syntax)RegexInvalidQuantifierRange
E3-005Regex (syntax)RegexStackedQuantifier
E3-006Regex (syntax)RegexEmptyToken
E3-007Regex (syntax)RegexUnknownToken
E3-008Regex (syntax)RegexInvalidEscape
E3-009Regex (syntax)RegexDanglingQuantifier
E3-010Regex (syntax)RegexUnexpectedCharacter
E3-011Regex (syntax)RegexPatternTooComplex
E3-101Regex (runtime)RegexStepLimitExceeded
E3-102Regex (runtime)RegexTimeout
E3-103Regex (runtime)RegexRecursionLimitExceeded
E4-001RuntimeUndefinedVariable
E4-002RuntimeUndefinedFunction
E4-003RuntimeConstantReassignment
E4-004RuntimeInvalidConstantName
E4-005RuntimeTypeMismatch
E4-006RuntimeDivisionByZero
E4-007RuntimeZeroIndexAccess
E4-008RuntimeIndexOutOfRange
E4-009RuntimeKeyNotFound
E4-010RuntimeArgumentCountMismatch
E4-011RuntimeNotCallable
E4-012RuntimeInvalidOperand
E4-013RuntimeLocalVariableOutOfScope
E4-014RuntimeInvalidCollectionOperation
E4-015RuntimeElementNotFound
E4-016RuntimeUnsupportedOperation
E4-017RuntimeStackOverflow
E4-018RuntimeNestedFunctionNotSupported
E4-019RuntimeAwaitOnNonAsync
E4-020RuntimeDeclarationTypeMismatch
E4-021RuntimeInvalidGlobalDeclaration
E4-022RuntimeReturnOutsideFunction
E4-023RuntimeStopOutsideLoop
E4-024RuntimeFractionalIndex
E4-025RuntimeInvalidArgumentValue
E4-026RuntimeSizeLimitExceeded
E4-027RuntimePureFunctionGlobalAccess
E4-028RuntimeNetworkRequestFailed
E4-029RuntimePureFunctionImpureCall
E4-030RuntimeParameterTypeMismatch
E5-001ModuleModuleNotFound
E5-002ModuleModuleParseFailed
E5-003ModuleCircularImport
E5-004ModuleUnknownDLC
E5-005ModuleDLCFeatureUnavailable
E5-006ModuleModuleAccessDenied
E5-007ModuleModuleLimitExceeded
E5-008ModuleFilesystemAccessDenied
E6-001ResourceExecutionStepLimit
E6-002ResourceExecutionTimeout
E6-003ResourceOutOfMemory
E9-001InternalInternalError

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.

TokenMeaning
[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~MExactly 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
ReDoS protection To prevent catastrophic backtracking from nested quantifiers that beginners easily write, such as ([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.

Every function name is "library_verb" 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.

LibraryFunctions
DLC:mathmath_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:stringstr_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:timetime_now, time_timestamp
DLC:randomrandom_float, random_int, random_seed, random_choice, random_shuffle
DLC:listlength, contains, index_of, list_sort, list_reverse, list_join, list_unique, list_sum, list_average, list_flatten, list_range
DLC:maplength, contains, map_keys, map_values, map_has_key, map_entries, map_merge
DLC:convertto_number, to_str, to_boolean
DLC:jsonto_json, from_json
DLC:networknetwork_get, network_post
DLC:filesystem
Blocked 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
What about this browser IDE? The behavior above is for the native 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
It is blocked outright in this browser IDE Unlike 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.
What if library names collide? You can load several DLCs at once, such as 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

MistakeWhy it's a problemHow 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 3The constant name isn't all upper case.Write it in capitals, like RETRIES.
Changing a constant with changeA 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 functionA 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 numberis / 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 endEvery 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:network sends 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:filesystem is available only in the native cuffc. In this browser IDE, a use DLC:filesystem in 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: async functions 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.