Welcome to Duso! This guide walks you through the language fundamentals and shows you how to write scripts.
The simplest way to get started: download a Duso binary and run a script file.
duso examples/hello.du
Or write a quick script from scratch:
echo 'print("Hello, Duso!")' > hello.du
duso hello.du
You can also run Duso in interactive mode (REPL):
duso -repl
Type commands and see results immediately. Use exit() to quit.
Duso supports various command-line flags for different workflows:
-read Browse files and docs interactively (start here for guided learning)-doc TOPIC Display formatted documentation for a module or builtin: duso -doc datastore-docserver Start a local webserver with searchable documentation-c CODE Execute inline code directly: duso -c 'print("Hello")'-repl Start interactive REPL mode for experimenting-debug Enable interactive debugger with breakpoints and watch expressions-init DIR Create a starter project structure in a directory-config OPTS Pass runtime configuration as key=value,key2=value pairs-extract SRC DST Extract files from the embedded virtual filesystem to disk-lib-path PATH Pre-pend a path to the module search path (for custom modules)-no-files Sandbox filesystem access: disables real filesystem access, restricting I/O to /EMBED/ (read-only embedded files) and /STORE/ (datastore virtual filesystem). Critical for running untrusted code like LLM-generated scripts.-stdin-port PORT Replace stdin/stdout with HTTP GET/POST (useful for sandboxed/containerized environments)-help Show the help message-lsp Start in Language Server Protocol mode (for editor integration)-no-stdin Disable stdin reading (useful for non-interactive execution)-no-color Disable ANSI color output in terminal-version Display the current Duso versionComments let you write notes in your code without affecting execution.
Single-line comments start with // and go to the end of the line:
// this is a comment
x = 5
print(x) // also a comment
Multi-line comments use /* ... */ and can span multiple lines:
/*
This is a block comment
that spans multiple lines
*/
print("Hello")
Multi-line comments support nesting, so you can comment out code that already contains comments:
/*
commenting out a function:
function calculate(a, b)
// add two numbers
return a + b
end
*/
See comments for details.
Duso is loosely typed—variables can hold any kind of value:
// string
name = "Alice"
// number
age = 30
score = 95.5
// booleab
active = true
// array
skills = ["Go", "Rust", "Python"]
// object
config = {timeout = 30, retries = 3}
Duso supports these types:
true or falseCheck a value's type with type():
// "number"
print(type(42))
// "string"
print(type("hello"))
// "array"
print(type([1, 2, 3]))
// "binary"
print(type(load_binary("data.bin")))
// "code"
print(type(parse("1")))
// "error"
print(type(parse("@")))
See print() to output values.
Binary values represent immutable binary data—raw bytes from files, images, or other binary content. They are memory-efficient because multiple references to the same binary data share the underlying pointer rather than copying data.
Load binary files with load_binary():
// Load a binary file
image = load_binary("avatar.png")
// Get size in bytes
size = len(image)
// Access metadata (like original filename)
filename = image["filename"]
// Save to another file
save_binary(image, "copy.png")
Binary values are particularly useful for efficiently passing large files between workers—multiple workers can hold references to the same binary data without copying:
// Load once, share with 1000 workers
binary_data = load_binary("large-file.bin")
for i = 1, 1000 do
// Each worker gets efficient pointer to same data
spawn("process_worker.du", {data = binary_data})
end
See Binary Type Reference for full details.
Code values represent compiled Duso code that can be executed dynamically. Create code values with the parse() function:
// Compile code without executing
code_val = parse("x = 10; x + 5")
// Check if parsing succeeded
if type(code_val) == "code" then
// It compiled successfully
result = eval(code_val)
print(result) // 15
end
Code values let you defer code execution or pass code between scopes:
// Dynamically compile and execute based on user input
user_formula = env("FORMULA") or "2 + 2"
code = parse(user_formula)
if type(code) == "error" then
print("Invalid code: " + code)
else
print("Result: " + eval(code))
end
See Code Type Reference for full details.
Error values represent parsing or runtime errors. Functions like parse() and catch can return error values:
// Parsing invalid syntax returns an error
result = parse("x = @") // Invalid syntax
if type(result) == "error" then
print("Parse error: " + result)
end
Errors are also created when exceptions are caught:
try
// This might fail
data = load("missing-file.json")
catch (err)
// err is of type "error"
if type(err) == "error" then
print("Error caught: " + err)
end
end
See Error Type Reference and Error Handling for full details.
Make decisions with if statements:
age = 25
if age >= 18 then
print("Adult")
elseif age >= 13 then
print("Teenager")
else
print("Child")
end
For quick conditional expressions, use the ternary operator (? and :):
status = age >= 18 ? "adult" : "minor"
See if for full details.
Loop through a range of numbers with for:
for i = 0, 4 do
print(i)
end
Iterate over arrays:
items = ["apple", "banana", "cherry"]
for item in items do
print(item)
end
Use while for condition-based loops:
count = 0
while count < 5 do
print(count)
count = count + 1
end
Skip iterations with continue or exit early with break:
// Prints: 1, 3-7
for i = 1, 10 do
if i == 2 then continue end
if i == 8 then break end
print(i)
end
See for and while for loop details.
Arrays are ordered lists, 0-indexed and mutable:
nums = [10, 20, 30]
// 10
print(nums[0])
// 3
print(len(nums))
// Mutable operations - modify array in place
// Add to end
push(nums, 40)
// [10 20 30 40]
print(nums)
// Functional operations - return new array
// [20 40 60 80]
doubled = map(nums, function(x) return x * 2 end)
print(doubled)
Add/remove elements with push(), pop(), shift(), and unshift().
Transform arrays with map(), filter(), sort(), and reduce().
See Array Type Reference.
Common array operations:
nums = [3, 1, 4, 1, 5, 9]
// Get length
len(nums)
// Access element at index
nums[0]
// Sort array (ascending)
nums = sort(nums)
// Sort with custom comparison (descending)
function desc(a, b)
return a > b
end
nums = sort(nums, desc)
// Add multiple elements to end (returns new length)
new_len = push(nums, 2, 6, 5)
// Remove and return last element
last = pop(nums)
// Add elements to beginning
unshift(nums, 0)
// Remove and return first element
first = shift(nums)
// Create sequence of numbers
range(1, 5)
// With step
range(0, 10, 2)
// Descending range
range(5, 0, -1)
// Get all keys from object
keys({name = "Alice", age = 30})
// Get all values from object
values({name = "Alice", age = 30})
Objects are key-value maps:
// define a simple object
person = {
name = "Alice",
age = 30,
city = "Portland"
}
// "Alice"
print(person.name)
// "Portland"
print(person["city"])
// Modify
person.age = 31
Objects can contain functions that act as methods. When you call a method with dot notation (obj.method()), the object is automatically bound, and the method can access the object's properties:
// a more complex object with methods
agent = {
name = "Alice",
age = 30,
greet = function(msg)
// methods can see parent object properties as variables
return msg + ", I am " + name + " (age " + age + ")"
end,
birthday = function()
// modifies the object's age property
age = age + 1
end
}
// prints: "Hello, I am Alice (age 30)"
print(agent.greet("Hello"))
// prints: "Hello, I am Alice (age 31)"
agent.birthday()
print(agent.greet("Hello"))
Methods can also call other methods on the same object:
worker = {
tasks_done = 0,
do_task = function()
tasks_done = tasks_done + 1
end,
do_two_tasks = function()
// Call another method
do_task()
do_task()
end,
status = function()
return "Completed " + tasks_done + " tasks"
end
}
worker.do_two_tasks()
// "Completed 2 tasks"
print(worker.status())
The same methods can work with different objects through the constructor pattern—each instance has its own properties while sharing method definitions.
Duso's unique object model allows objects to act as constructors/factories. Call an object with () to create a shallow copy, optionally overriding fields:
config = {timeout = 30, retries = 3}
// Creates new copy with defaults
config1 = config()
// Override specific fields using named arguments
config2 = config(timeout = 60)
config3 = config(timeout = 60, retries = 5)
This is particularly powerful for creating "instances" with shared behavior:
// Blueprint: method definitions + default state
Counter = {
count = 0,
increment = function()
count = count + 1
end,
reset = function()
count = 0
end,
value = function()
return count
end
}
// Create two independent counters from the blueprint
counter1 = Counter()
counter2 = Counter()
counter1.increment()
counter1.increment()
print(counter1.value()) // 2
counter2.increment()
print(counter2.value()) // 1 (independent!)
Each instance has its own copy of the state (count), but all share the same method definitions. This gives you object-oriented-like behavior without needing a class keyword.
Arrays also work as constructors, creating shallow copies with optional elements appended via positional arguments:
// Template array
template = [1, 2, 3]
// Use as a template to make more arrays
copy = template()
extended = template(4, 5)
// copy = [1, 2, 3]
// extended = [1, 2, 3, 4, 5]
Arrays can only be called with positional arguments (appending elements), unlike objects which support named field overrides.
When you create a copy with the constructor pattern (obj() or arr()), you get a shallow copy—nested structures are shared:
original = {scores = [10, 20, 30]}
copy = original()
// Modifies both original and copy!
copy.scores[0] = 999
// 999
print(original.scores[0])
For deep copies where nested structures are independent, use deep_copy():
original = {scores = [10, 20, 30]}
copy = deep_copy(original)
// Only affects the copy
copy.scores[0] = 999
// 10 (unchanged)
print(original.scores[0])
Each script invoked via spawn() or run() runs in its own isolated scope. When passing objects between scopes (as arguments to spawned scripts or return values), Duso automatically performs a deep copy to prevent stale closures.
deep_copy() removes functions from objects because:
// Parent scope
multiply = function(x) return x * 2 end
obj = {
value = 10,
transform = multiply // closure tied to parent scope
}
// Spawned child scope
worker = spawn("worker.du", {data = obj})
// data.transform is automatically stripped during deep copy
// because it can't work in the child's isolated scope
If your spawned script needs transformation logic, pass it as a separate function or use the datastore for coordination instead.
When you pass data to spawn(), run(), or datastore(), Duso automatically performs a deep copy for isolation. Understanding what survives is critical for orchestration:
~pattern~ if needed in spawned scripts// Parent scope
worker_data = {
name = "worker",
tasks = [1, 2, 3],
pattern = ~\w+~, // OK - becomes string "\\w+"
process = function() end // STRIPPED - becomes nil
}
// Spawn child
pid = spawn("worker.du", {data = worker_data})
// In worker.du:
ctx = context()
data = ctx.request().data
print(data.name) // "worker" ✓
print(data.tasks) // [1 2 3] ✓
print(data.pattern) // "\\w+" (now a string, not regex) ✓
print(data.process) // nil ✗ function was stripped
Best Practice: If spawned scripts need behavior, pass behavior via separate parameters or module imports, not embedded functions. Use datastore for shared coordination instead of shared state.
Use keys() and values() to extract object contents:
config = {host = "localhost", port = 8080}
// [host port]
print(keys(config))
// [localhost 8080]
print(values(config))
Define functions with the function keyword and return values with return:
function greet(name)
return "Hello, " + name
end
// "Hello, World"
print(greet("World"))
You can assign functions to variables:
double = function(x)
return x * 2
end
// 10
print(double(5))
Call functions with positional or named arguments:
function configure(timeout, retries, verbose)
return {timeout = timeout, retries = retries, verbose = verbose}
end
// Positional
configure(30, 3, true)
// Named
configure(timeout = 60, retries = 5)
// Mixed
configure(30, verbose = false)
Function parameters can have default values, which are used when arguments are not provided:
function greet(name, greeting = "Hello", punctuation = "!")
return greeting + " " + name + punctuation
end
// Hello Alice!
print(greet("Alice"))
// Hi Bob!
print(greet("Bob", "Hi"))
// Hey Charlie?
print(greet("Charlie", "Hey", "?"))
Default values work with all calling styles (positional, named, and mixed).
Functions capture their surrounding scope at definition time.
function makeAdder(n)
function add(x)
// Captures n from outer scope at definition time
return x + n
end
return add
end
addFive = makeAdder(5)
// 15
print(addFive(10))
// 25
print(addFive(20))
Variables captured from the outer scope remain live:
function makeCounter()
var count = 0
return function()
// Modifies the captured variable
count = count + 1
return count
end
end
counter = makeCounter()
// 1
print(counter())
// 2
print(counter())
// 3
print(counter())
Each closure maintains its own captured environment.
One of Duso's strengths is template strings—embed expressions directly in strings with {{...}}:
name = "Alice"
age = 30
message = "{{name}} is {{age}} years old"
// "Alice is 30 years old"
print(message)
Templates work with any expression—arithmetic, function calls, conditionals:
nums = [1, 2, 3, 4, 5]
// "Sum=3"
msg = "Sum={{nums[0] + nums[1]}}"
status = "Age: {{age >= 18 ? "adult" : "minor"}}"
For longer text, use triple quotes """...""" to preserve newlines:
doc = """
This is a multiline string.
It preserves newlines naturally.
No escaping needed!
"""
Indentation is automatically handled—extra indents that match your code are filtered out:
function format_output(name, data)
return """
User: {{name}}
Status: {{data.status}}
Score: {{data.score}}
"""
end
The leading spaces from the code indentation are removed from the final string.
Combine templates with multiline strings for structured text like JSON or Markdown:
name = "Alice"
score = 95
// Generate Markdown
report = """
# Report for {{name}}
Score: {{score}}
Grade: {{score >= 90 ? "A" : score >= 80 ? "B" : "C"}}
Generated at: {{format_time(now(), "iso")}}
"""
print(report)
Perfect for generating JSON, SQL, HTML, Markdown, or any structured text without escaping quotes or worrying about newlines.
See String Type Reference for more details.
Common string operations:
text = "Hello World"
// Convert to uppercase
upper(text)
// Convert to lowercase
lower(text)
// Get length
len(text)
// Extract substring (start, optional length)
substr(text, 0, 5)
// Extract from position to end
substr(text, 6)
// Negative indices from end
substr(text, -5)
// Split by delimiter
split(text, " ")
// Split into individual characters
split(text, "")
// Join array with separator
join(["Hello", "World"], "-")
// Remove leading/trailing whitespace
trim(" spaces ")
// Remove whitespace including tabs and newlines
trim("\t hello \n")
// Replace all occurrences
replace(text, "World", "Duso")
// Case-insensitive replacement
replace(text, "hello", "hi", ignore_case = true)
// Check if contains pattern
contains(text, "World")
// Check if starts with prefix
starts_with(text, "Hello")
// Check if ends with suffix
ends_with(text, "World")
// Case-insensitive prefix/suffix check
starts_with(text, "hello", ignore_case = true)
ends_with(text, "world", ignore_case = true)
// Find all matches (returns array of {text, pos, len})
matches = find(text, ~\w+~)
See String Type Reference for more details.
Duso supports regular expressions using Go's regex syntax, delimited with ~...~:
email = "[email protected]"
if contains(email, ~\w+@\w+\.\w+~) then
print("Valid email format")
end
Use find() to locate all matches in a string:
text = "The years 2020, 2021, and 2022 were busy"
matches = find(text, ~\d+~)
for match in matches do
// "2020", "2021", "2022"
print(match.text)
// Position in string
print(match.pos)
// Length of match
print(match.len)
end
Use replace() to replace all matches:
text = "Hello 123 World 456"
cleaned = replace(text, ~\d+~, "X")
// "Hello X World X"
print(cleaned)
Replace with a function to transform matches:
text = "apple, banana, cherry"
formatted = replace(text, ~\w+~, function(text, pos, len)
// Function receives text, position, and length
return upper(text)
end)
// "APPLE, BANANA, CHERRY"
print(formatted)
Use contains() to check if a pattern exists:
phone = "555-1234"
if contains(phone, ~\d{3}-\d{4}~) then
print("Looks like a phone number")
end
Patterns are case-sensitive by default, but you can pass true as the third argument for case-insensitive matching:
if contains("HELLO", ~hello~, true) then
print("Match found")
end
Some useful regex patterns for common tasks:
// Email-like pattern
email_pattern = ~[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}~
// Numbers
number_pattern = ~\d+(\.\d+)?~
// Whitespace
space_pattern = ~\s+~
// Word characters
word_pattern = ~\w+~
// URLs
url_pattern = ~https?://[^\s]+~
See Go's regexp documentation for the full syntax reference.
Use try and catch to handle errors gracefully:
try
data = load("config.json")
catch (error)
print("Failed to load: " + error)
data = {}
end
The error message is captured as a string and you can handle it however you need.
Use throw() to raise an error from your code. throw() accepts any data type—strings, objects, arrays—allowing you to create rich, app-specific error responses:
function validate(age)
if age < 0 then
throw("Age cannot be negative")
end
return age
end
try
result = validate(-5)
catch (e)
print("Error: " + e)
end
For orchestration and agent workflows, use objects to provide detailed error information:
function fetch_user(id)
if id == nil then
throw({
code = "INVALID_ID",
message = "User ID is required",
status_code = 400
})
end
// ... fetch logic
end
function process_batch()
for user_id in user_ids do
try
user = fetch_user(user_id)
// process user
catch (err)
// err can be a string, object, array, or any type
if type(err) == "object" then
print("Code: " + err.code + ", Message: " + err.message)
else
print("Error: " + err)
end
end
end
end
This approach lets you pass domain-specific error information between functions and across process boundaries (via run() and spawn()), making error handling richer and more contextual.
Use breakpoint() to pause execution and inspect state (when running with -debug flag):
x = 42
// Execution pauses here in debug mode
breakpoint()
// Breakpoint with values (works like print())
user = {id = 123, name = "Alice"}
breakpoint("user:", user)
// Conditional breakpoint
for i = 1, 100 do
if i == 50 then
// Pause only at i=50
breakpoint("Found i={{i}}")
end
end
Use watch() to monitor expressions and break when they change:
count = 0
for i = 1, 100 do
count = count + 1
// Breaks when count changes
watch("count")
end
// Watch multiple expressions
// Monitors x, the boolean x > 5, and array length
watch("x", "y > 5", "len(items)")
See breakpoint() and watch() for full details on debugging.
By default, assignment looks up the scope chain. Use var to explicitly create a local variable:
x = 10
function test()
// Modifies outer x
x = x + 1
end
test()
// 11
print(x)
function create_local()
// New local x, shadows outer
var x = 100
x = x + 1
end
create_local()
// Still 11 (outer x unchanged)
print(x)
Function parameters and loop variables are automatically local.
Load reusable code with require():
claude = require("claude")
response = claude.prompt("What is 2 + 2?")
Or execute a script in your current scope with include():
include("helpers.du")
// Now available
result = helper_function()
Modules are cached—subsequent requires return the same value.
For a complete list of available standard library and contrib modules, see Module Reference.
duso-modulename (e.g., duso-postgres)contrib/ and baked into the binaryFor detailed contribution guidelines, see CONTRIBUTING.md and contrib/README.md.
Use -lib-path to add custom module search directories:
duso -lib-path ./my_modules script.du
my_util = require("my_modules/utils")
claude module with full API feature supportFor now, if you need other LLM providers, integrate via HTTP calls using fetch():
// Custom integration example
result = fetch("https://api.openai.com/v1/chat/completions", {
method = "POST",
headers = {["Authorization"] = "Bearer " + env("OPENAI_API_KEY")},
body = format_json({
model = "gpt-4",
messages = [{role = "user", content = "Hello"}]
})
})
response = parse_json(result.body)
print(response.choices[0].message.content)
We're actively expanding the module ecosystem and LLM provider support with community contributions. If you'd like to contribute a module or suggest a provider, reach out!
See require() and include() for details.
Duso includes built-in Claude integration via a module. Load it with require():
claude = require("claude")
// Simple prompt
response = claude.prompt("What is 2 + 2?")
print(response)
// Multi-turn conversation
analyst = claude.session(
system = "You are a data analyst. Be concise."
)
result = analyst.prompt("Analyze this data")
You can also enable Claude to use tools—giving it access to functions it can call to answer questions (agent patterns):
claude = require("claude")
// Define a tool Claude can use
var web_search = {
name = "web_search",
description = "Search the web for information",
input_schema = {
type = "object",
properties = {
query = {type = "string", description = "Search query"}
},
required = ["query"]
}
}
// Create an agent that can call your tools
agent = claude.session({
tools = [web_search],
tool_handlers = {
web_search = function(input)
// In a real app, this would call a web search API
return "Search results for: " + input.query
end
}
})
// Claude will automatically call tools when needed
response = agent.prompt("What's the latest on Duso?")
print(response)
Claude automatically executes tool calls and integrates results into the conversation. The claude module makes it easy to orchestrate multi-step AI workflows with tool use loops. See Claude Module Documentation for full details on tools, handlers, and manual tool control.
As we add support for OpenAI, Gemini, and other providers, they will follow the same module pattern:
// Coming soon - these will work the same way
openai = require("openai")
response = openai.prompt("Your question", {model = "gpt-4"})
gemini = require("gemini")
response = gemini.prompt("Your question", {model = "gemini-2.0"})
Community contributions for additional LLM providers are welcome! See the LLM Provider Support section for details.
Read and write files:
// Read entire file
content = load("config.json")
// Write to file (create or overwrite)
save("output.txt", "Hello, World!")
// Append to file
append_file("log.txt", "New log entry\n")
// Copy file
copy_file("source.txt", "destination.txt")
// Move/rename file
move_file("old_name.txt", "new_name.txt")
rename_file("old.txt", "new.txt")
// Delete file
remove_file("temp.txt")
// Check if file/directory exists
if file_exists("data.txt") then
print("File found")
end
// Get file type
file_type("data.txt")
// List directory contents
files = list_dir("./data")
// Create directory (including parents)
make_dir("./output/nested/path")
// Remove empty directory
remove_dir("./empty_folder")
// Get current working directory
pwd = current_dir()
Access system environment and configuration:
// Read environment variable
api_key = env("API_KEY")
// Provide fallback if not set
db_host = env("DB_HOST") or "localhost"
// Read multiple settings
config = {
host = env("HOST") or "0.0.0.0",
port = tonumber(env("PORT") or "8080"),
debug = env("DEBUG") == "true"
}
Make HTTP requests with the fetch() builtin:
// Simple GET request
response = fetch("https://api.example.com/users")
if response.ok then
data = response.json()
print(data)
end
// POST with data
result = fetch("https://api.example.com/users", {
method = "POST",
headers = {["Content-Type"] = "application/json"},
body = format_json({name = "Alice", age = 30})
})
The fetch() function provides a JavaScript-style API for making HTTP requests. Connection pooling is handled automatically by the runtime.
See fetch() reference for full details.
Create HTTP servers with the http_server() builtin:
// Server setup mode
server = http_server({port = 8080})
// setup routes to other scripts
server.route("GET", "/", "handlers/home.du")
server.route("GET", "/api/users", "handlers/users.du")
print("Server listening on http://localhost:8080")
// Blocks until Ctrl+C
server.start()
// we're done
print("Server stopped")
For simple applications, a single script can be both the server setup and its own handler (self-referential pattern):
// get our script context (process info)
ctx = context()
// if we're the main script instance, start server
if ctx == nil then
server = http_server({port = 8080})
// setup routes to this script
server.route("GET", "/")
// start server and wait until it finishes
server.start()
// we're done, user hit Ctrl+C
exit()
end
// if we got here, we're our own handler
// run in a separate child process
// get req/res info
req = ctx.request()
res = ctx.response()
// send back a simple HTML response
res.html("Hello World!")
// OR
// some json using duso object/arrays
res.json({
success = true,
data = "Hello World!"
})
See http_server() reference for full details.
Run other scripts synchronously with run() or asynchronously with spawn():
result = run("processor.du", {data = [1, 2, 3]})
print("Result: " + format_json(result))
pid1 = spawn("worker1.du", {data = things})
pid2 = spawn("worker2.du", {data = things})
// Main script continues immediately
print("Spawned workers with PIDs: " + pid1 + ", " + pid2)
Scripts use exit() to return values:
// worker.du
exit({status = "done", value = 42})
This works in all contexts:
exit(response_object) sends HTTP responserun() scripts: exit(value) becomes the return valuespawn() scripts: exit(value) completes the scriptA single script can work both standalone and as a handler using the gate pattern:
ctx = context()
if ctx == nil then
// Standalone: spawn other scripts or start server
result = run("child.du", {config = {...}})
print("Child returned: " + format_json(result))
else
// Handler mode: process the request/spawn context
stack = ctx.callstack()
print("Called from: " + stack[0].filename)
exit({status = "done"})
end
Use context().callstack() for debugging to see the invocation chain (HTTP request, run, spawn, etc.).
For scripts that spawn multiple workers, use datastore() for safe coordination without shared memory:
// Orchestrator: spawn 5 workers
store = datastore("job_123")
store.set("completed", 0)
pids = []
for i = 1, 5 do
pid = spawn("worker.du", {job_id = "job_123", worker_num = i})
push(pids, pid)
end
// Wait for all workers to finish
store.wait("completed", 5)
print("All workers done! PIDs were: " + format_json(pids))
// worker.du - each spawned script
ctx = context()
job_id = ctx.request().job_id
store = datastore(job_id)
// Atomic operation
store.increment("completed", 1)
Datastores are thread-safe key/value stores that support:
increment(), push() no race conditionswait(key, value) efficient blocking until value changesBy default, datastores are in-memory and reset when the process exits. For persistent coordination across restarts, use disk storage:
// In-memory (default)
store = datastore("job_123")
// With optional disk persistence
store = datastore("job_123", {disk = true})
All blocking operations support optional timeouts to prevent indefinite hangs:
store = datastore("jobs")
// Wait with 30-second timeout (returns nil if timeout exceeded)
result = store.wait("completed", 5, timeout = 30)
// Other blocking calls also support timeouts:
value = store.pop(timeout = 10)
value = store.shift(timeout = 10)
Pass a function to wait() to check complex conditions:
store = datastore("metrics")
store.set("temperature", 18)
// Wait until temperature reaches safe level (>= 20)
try
result = store.wait("temperature", function(temp)
return temp >= 20
end, 30)
print("Temperature is safe: " + result)
catch (err)
print("Timeout waiting for safe temperature")
end
This pattern scales from 2 workers to 1000+ workers with the same clean code. The datastore handles all concurrency - no locks needed in your scripts.
See datastore() for full API and examples.
Duso includes functions for transforming data:
Transform each element:
nums = [1, 2, 3, 4, 5]
squared = map(nums, function(x) return x * x end)
// [1 4 9 16 25]
print(squared)
Keep only matching elements:
evens = filter(nums, function(x) return x % 2 == 0 end)
// [2 4]
print(evens)
Combine elements into a single value:
sum = reduce(nums, function(acc, x) return acc + x end, 0)
// 15
print(sum)
Chain operations together for powerful transformations:
result = map(
filter([1, 2, 3, 4, 5, 6], function(x) return x % 2 == 0 end),
function(x) return x * 10 end
)
// [20 40 60]
print(result)
These functions work great together—see map(), filter(), and reduce() for more examples.
For independent operations (like multiple API calls), use parallel():
claude = require("claude")
results = parallel([
function()
return claude.prompt("Explain machine learning")
end,
function()
return claude.prompt("Explain deep learning")
end,
function()
return claude.prompt("Explain neural networks")
end
])
print(results[0])
print(results[1])
print(results[2])
Each function runs concurrently. If one errors, that result becomes nil.
See parallel() for more details.
Parse JSON responses from APIs:
json_str = """{"name": "Alice", "age": 30}"""
data = parse_json(json_str)
// "Alice"
print(data.name)
Convert Duso values to JSON:
person = {name = "Bob", age = 25}
json = format_json(person)
// {"name":"Bob","age":25}
print(json)
// Pretty-printed with 2-space indent
pretty = format_json(person, 2)
Perfect for working with LLM responses and APIs.
Use parse_json() to parse JSON strings and format_json() to convert values to JSON.
Work with Unix timestamps:
// Current timestamp
now_ts = now()
formatted = format_time(now_ts, "YYYY-MM-DD")
// "2026-02-14" (example output)
print(formatted)
// Parse a date string to timestamp
ts = parse_time("2026-01-22")
Use now() to get the current timestamp, format_time() to format timestamps, and parse_time() to parse date strings.
Duso includes mathematical functions for basic operations, trigonometry, and advanced calculations.
Common mathematical operations:
// 42
print(abs(-42))
// 4
print(sqrt(16))
// 8
print(pow(2, 3))
// 3
print(floor(3.7))
// 4
print(ceil(3.2))
// 4
print(round(3.5))
All trigonometric functions work with angles in radians. Use pi() to work with radians:
// Convert degrees to radians
degrees = 45
radians = degrees * pi() / 180
// Calculate trigonometric functions
// ~0.707
print(sin(radians))
// ~0.707
print(cos(radians))
// ~1
print(tan(radians))
// Inverse functions (return radians)
// Angle of point (1, 1)
angle = atan2(1, 1)
// 45 degrees
print(angle * 180 / pi())
Common uses:
// Circular motion - calculate position on a circle
radius = 100
for i in range(0, 360, 45) do
angle = i * pi() / 180
x = radius * cos(angle)
y = radius * sin(angle)
print("{{i}}°: ({{x}}, {{y}})")
end
// Find angle between two points
x1 = 0
y1 = 0
x2 = 3
y2 = 4
angle = atan2(y2 - y1, x2 - x1)
distance = sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2))
print("Angle: {{angle}}, Distance: {{distance}}")
Growth, decay, and scale calculations:
// Exponential growth
population = 1000
growth_rate = 0.05
years = 10
final_population = population * exp(growth_rate * years)
print("Population: {{final_population}}")
// Find logarithms
// 2 (base 10)
print(log(100))
// ~1 (natural log)
print(ln(2.71828))
// Inverse relationship
x = 5
// 5
print(ln(exp(x)))
load()content = load("data.txt")
save()save("output.json", data)
list_files()scripts = list_files("*.du")
backups = list_files("/STORE/*.bak")
Virtual Filesystems: Duso supports two virtual filesystems:
/EMBED/Read-only embedded resources (baked into the binary)/STORE/Read-write virtual filesystem backed by the datastore (survives across runs if using persistent datastores)These are essential for secure execution. Use
duso -no-filesto sandbox scripts to ONLY these virtual filesystems, blocking all real filesystem and environment access. Perfect for running untrusted code (like LLM-generated scripts). Learn more in the Virtual Filesystems Guide.
parse_json()data = parse_json(response)
format_json()json = format_json(data)
markdown_html(), markdown_ansi(), markdown_text()md = "# Title\n\n**Bold** text"
// Render to HTML
html = markdown_html(md)
// Render with terminal colors
ansi = markdown_ansi(md)
// Render to plain text
text = markdown_text(md)
fetch() builtinresponse = fetch("https://api.example.com/endpoint")
if response.ok then
data = response.json()
end
claude moduleclaude = require("claude")
response = claude.prompt("Your question here")
try
result = risky_operation()
catch (error)
print("Error: " + error)
end
for item in items do
print(item)
end
map()doubled = map(numbers, function(x) return x * 2 end)
// Objects: copy with optional named overrides
config = {timeout = 30, retries = 3}
// Shallow copy
copy = config()
// Copy with override
modified = config(timeout = 60)
// Arrays: copy with optional positional appends
template = [1, 2, 3]
// Shallow copy
copy = template()
// Copy with appended elements
extended = template(4, 5)
deep_copy()// Independent nested copies
independent = deep_copy(original)
examples/core/ for feature demonstrationsYou can also read reference for keywords and builtin functions using the binary itself:
duso -doc TERM
Or run Duso as a local web server with full documentation:
duso -docserver
Happy scripting!