# CascadaScript Documentation [Cascada GitHub Project](https://github.com/geleto/cascada) [Download as Markdown](https://raw.githubusercontent.com/geleto/cascada/master/docs/cascada/script.md) [Markdown for AI Coding Agents - Script and Template syntax](https://raw.githubusercontent.com/geleto/cascada/refs/heads/master/docs/cascada/cascada-agent.md) CascadaScript inverts the traditional programming model: it is concurrent by default, sequential only when explicitly asked. Everything runs at once - all statements, each part of every expression, every operation in each call, each iteration of every loop - an operation only waits when it depends on another's result. What makes it extraordinary is how ordinary the syntax looks - instantly familiar to any JavaScript or Python developer. And the result is identical to sequential execution. ## CascadaScript - Implicitly Concurrent, Explicitly Sequential CascadaScript is a specialized scripting language designed for orchestrating complex asynchronous workflows in JavaScript and TypeScript applications. It is not a general-purpose programming language; instead, it acts as a data-orchestration layer for coordinating APIs, databases, LLMs, and other I/O-bound operations with maximum concurrency and minimal boilerplate. It uses familiar syntax and language constructs, while offering language-level support for boilerplate-free concurrent workflows, explicit control over side effects, deterministic output construction, and dataflow-based error handling with recovery rollbacks. **⚠️ Under active development:** Cascada is evolving rapidly - bugs are possible. Issues and contributions are very welcome. The core execution model: * ⚡ **Concurrent by default** - Independent operations - variable assignments, function calls, loop iterations - execute concurrently without `async`, `await`, or promise management. * 🚦 **Data-driven execution** - Code runs automatically when its input data becomes available, eliminating race conditions by design. * ➡️ **Explicit sequencing only when needed** - Order specific calls, loops, or external interactions with dedicated language constructs - the rest of the script stays concurrent. * 📋 **Deterministic outputs** - Even though execution is concurrent and often out-of-order, Cascada guarantees that final outputs are assembled exactly as if the script ran sequentially. * ☣️ **Errors are data** - Failures propagate through the dataflow instead of throwing exceptions, allowing unrelated concurrent work to continue safely. CascadaScript is particularly well suited for: * AI and LLM orchestration * Data pipelines and ETL workflows * Agent systems and planning patterns * High-throughput I/O coordination In short, Cascada lets developers write clear, linear logic while the engine handles concurrent execution, ordering guarantees, and error propagation automatically. Despite executing concurrently by default, it reads exactly like the synchronous code you already write: ```javascript var user = fetchUser(userId) // ┐ start immediately, var posts = fetchPosts(userId) // ┘ run concurrently // evaluates as soon as 'user' resolves - posts may still be fetching var role = "admin" if user.isAdmin else "member" // for loop - every iteration runs concurrently data result // writes are concurrent, output is assembled in source order for post in posts var enriched = enrichPost(post) result.posts.push({ title: enriched.title | title, status: "published" if enriched.isLive else "draft" }) endfor // ! makes these sequential with each other, without breaking concurrency with the rest db!.log("report", userId) db!.updateLastSeen(userId) return { name: user.name, role: role, posts: result.snapshot() } // snapshot waits for all writes ``` Every construct above runs exactly as you'd read it - the engine orchestrates all the async concurrency. ## Read First **Articles:** - [CascadaScript Introduction](https://geleto.github.io/posts/cascada-script-intro/) - An introduction to CascadaScript's syntax, features, and how it solves real async programming challenges - [The Kitchen Chef's Guide to Concurrent Programming with Cascada](https://geleto.github.io/posts/cascada-kitchen-chef/) - Understand how Cascada works through a restaurant analogy - no technical jargon, just cooks, ingredients, and a brilliant manager who makes concurrent execution feel as natural as following a recipe **Learning by Example:** - [Casai Examples Repository](https://github.com/geleto/casai-examples) - Explore practical examples showing how Cascada and Casai (an AI orchestration framework built on Cascada) turn complex agentic workflows into readable, linear code - no visual node graphs or async spaghetti, just clear logic that tells a story (work in progress) ## Table of Contents - [Quick Start](#quick-start) - [Cascada's Execution Model](#cascadas-execution-model) - [Language Fundamentals](#language-fundamentals) - [Control Flow](#control-flow) - [Channels](#channels) - [Sequencing External Interactions](#sequencing-external-interactions) - [Functions and Reusable Components](#functions-and-reusable-components) - [Error Handling](#error-handling) - [Return Statements](#return-statements) - [Composition and Loading](#composition-and-loading) - [API Reference](#api-reference) - [Development Status and Roadmap](#development-status-and-roadmap) ## Quick Start ```bash npm install cascada-engine ``` ### The script Write plain, familiar logic. Cascada runs independent operations concurrently: ```javascript const script = ` var user = fetchUser(userId) var posts = fetchPosts(userId) return { name: user.name, postCount: posts.length } `; ``` No `async`, no `await`. `fetchUser` and `fetchPosts` run concurrently - Cascada handles it. ### Running a script Pass the script and a context object to `renderScriptString`. Any value in the context can be a promise or an async function: ```javascript import { AsyncEnvironment } from 'cascada-engine'; const env = new AsyncEnvironment(); const result = await env.renderScriptString(script, { userId: 123, fetchUser: (id) => db.users.findById(id), fetchPosts: (id) => db.posts.findByUser(id) }); console.log(result); // { name: 'Alice', postCount: 5 } ``` To understand how Cascada achieves effortless concurrency, read the next section. ## Cascada's Execution Model Cascada's approach to concurrency inverts the traditional programming model. Understanding this execution model is essential to writing effective CascadaScripts - it explains why the language behaves the way it does and how to leverage its concurrency. #### Concurrent by default Cascada fundamentally inverts the traditional programming model: instead of being sequential by default, Cascada is concurrent by default. Independent variable assignments, function calls, loop iterations, and function invocations all run concurrently - no special syntax required. #### Data-Driven Flow: Code runs when its inputs are ready. In Cascada, any independent operations - like API calls, LLM requests, and database queries - are automatically executed concurrently without requiring special constructs or even the `await` keyword. The engine intelligently analyzes your script's data dependencies, guaranteeing that operations will wait for their required inputs before executing. This applies to all constructs: expressions evaluate as soon as their operands resolve, conditionals wait for their condition, loops wait for their iterable, and function calls wait for their arguments. This orchestration eliminates the possibility of race conditions by design, ensuring correct execution order while maximizing performance for I/O-bound workflows. #### Implicit Concurrency: Write Business Logic, Not Async Plumbing. Forget await. Forget .then(). Forget manually tracking which variables are promises and which are not. Cascada fundamentally changes how you interact with asynchronous operations by making them invisible. This "just works" approach means that while any variable can be a promise under the hood, you can pass it into functions, use it in expressions, and assign it without ever thinking about its asynchronous state. #### Implicitly Concurrent, Explicitly Sequential While this "concurrency-first" approach is powerful, some operations still need to run in a specific order. Cascada can order its own internal work automatically, including data/text channel assembly and dependencies between script expressions. The hard case is imported native functions and objects from the render context: APIs, mutable object methods, database handles, file writers, LLM clients, and helpers that read or change shared state. Cascada cannot know whether those functions are pure or side-effectful, so you mark the ordering explicitly. For these cases you have three tools: the `!` marker, which enforces strict sequential order on a specific context-object path (such as database writes or stateful API calls); the `each` loop, which iterates a collection one item at a time when per-item side-effects must not overlap; and the `sequence` construct, which provides a named sequence object for strictly ordered reads and calls on an external context object while still returning each call's value. All three are surgical - they sequence only what they touch, without affecting the concurrency of the rest of the script. #### Execution is chaotic, but the result is orderly While independent operations run concurrently and may start and complete in any order, Cascada guarantees the final output is identical to what you'd get from sequential execution. This means all your data manipulations are applied predictably, ensuring your final texts, arrays and objects are assembled in the exact order written in your script. #### Dataflow Poisoning - Errors that flow like data Cascada replaces traditional try/catch exceptions with a data-centric error model called dataflow poisoning. If an operation fails, it produces an `Error Value` that propagates to any dependent operation, variable and output - ensuring corrupted data never silently produces incorrect results. For example, if fetchPosts() fails, any variable or output using its result also becomes an error - but critically, unrelated operations continue running unaffected. Poisoning is conservative with control flow: if an `if` condition is an Error Value, neither branch runs and every variable that either branch would have modified becomes poisoned. You can detect and repair these errors using `is error` checks, providing fallbacks and logging without derailing your entire workflow. #### Clean, Expressive Syntax CascadaScript offers a modern, expressive syntax designed to be instantly familiar to JavaScript and TypeScript developers. It provides a complete toolset for writing sophisticated logic, including variable declarations (`var`), `if/else` conditionals, `for/while` loops, and a full suite of standard operators. Build reusable components with `function`, which supports default values and keyword arguments, and compose complex applications by organizing your code into modular files with `import` and `extends`. ## Language Fundamentals ### Features at a Glance What makes CascadaScript remarkable is how unremarkable it looks. Despite executing concurrently by default, the language offers the same familiar constructs found in Python, JavaScript, and similar languages - but without the async keyword, no callbacks, no promise chains. You write straightforward sequential-looking logic, the engine handles the concurrency. | Feature | Syntax | Notes | |---|---|---| | Variable declaration | `var name = value` | Always declare before use with `var` | | Assignment | `name = value`, `obj.prop = value` | Assign or reassign a variable or property to a new value | | Arithmetic | `+`, `-`, `*`, `/`, `//`, `%`, `**` | `+` also concatenates `string + string`; `//` is integer division, `**` is exponentiation | | Comparisons | `==`, `!=`, `<`, `>`, `<=`, `>=`, `===` | Standard comparisons | | Logic | `and`, `or`, `not` | Word-form boolean operators | | Strings | `"text"`, `'text'` | `+` concatenates two strings; `~` explicitly stringifies text-like values | | Arrays | `[1, 2, 3]` | Array literals | | Objects / dicts | `{key: "value"}` | Object literals | | Expressions | `obj.prop`, `arr[i]`, `2*x + 1` | Member access, indexing, compound expressions; any expression is a valid standalone statement | | Inline if (ternary) | `a if condition else b` | Python-style conditional expression | | Conditionals | `if / elif / else / endif` | Standard branching | | Switch | `switch / case / default / endswitch` | Multi-way branching | | Concurrent loop | `for item in array`, `for key, value in object`, `for element in iterator` | Iterations run concurrently | | Sequential loop | `each item in list / endeach` | Iterations run in strict order | | While loop | `while condition / endwhile` | Condition-based loop | | Filters | `value \| filterName(args)` | Transform values with built-in or custom filters | | Function calls | `funcName(a, b)` | Call script functions, context functions, globals, or inherited methods | | Functions | `function name(arg, optional="x") ... endfunction` | Define reusable callable functions; supports default values and keyword arguments | | Methods | `method name(args) … endmethod` | Define overridable, value-returning methods for `extends` chains | | Imports | `import "file" as ns`, `from "file" import name` | Import a namespace or specific functions from another script | | Comments | `// line`, `/* block */` | Standard comment syntax | Everything above is the language you already know. Cascada adds a small set of simple purpose-built constructs on top: | Cascada Feature | Syntax | Purpose | |---|---|---| | Implicit concurrency | *(no syntax)* | Independent operations run concurrently automatically | | `text` channel | `text log`, `log("line")` | Generate text from concurrent code, assembled in source order | | `data` channel | `data out`, `out.items.push(item)` | Build structured objects and arrays from concurrent code - writes are concurrent, result is in source order | | `sequence` construct | `sequence db = services.db`, `var user = db.getUser(1)` | Create a named sequence object for sequential reads and calls on an external object | | Sequential operator | `obj!.method()`, `obj!.prop` | Enforce strict execution order on a context object path | | Guard | `guard [targets] / recover [err] / endguard` | Transaction-like block: auto-restores state on error | | Dataflow error poisoning | `value is error`, `value#message` | Failures propagate as error values through the dataflow; unrelated operations continue unaffected. If a control-flow condition is an error, all writes that would have happened in the skipped branches become poisoned too. Detect with `is error`, inspect with `#` | ### Core Syntax and Expressions - **Multiline Expressions**: Expressions can span multiple lines for readability. The system automatically detects continuation based on syntax (e.g., unclosed operators, brackets, or parentheses). For example: ``` var result = 5 + 10 * 20 - 3 ``` - **Standard Comments**: Use JavaScript-style comments (`//` and `/* */`) - **Code**: Any standalone line that isn't a recognized command (e.g., `var`, `if`, `for`, `import`) or tag is treated as an expression. For example: ``` computeTotal(items, tax) ``` ### Variable Declaration and Assignment CascadaScript uses a strict and explicit variable handling model that separates declaration from assignment for better clarity and safety. #### Declaring Local Variables with `var` Use `var` to declare a new, script-local variable. Re-declaring a variable that already exists in a visible scope will cause a compile-time error. If no initial value is provided, the variable defaults to `none`. This rule applies to all declaration-producing binders, not just `var` - including loop targets, call-block parameters, `import`/`from import` names, component aliases, and `recover` bindings. Identifier names may contain letters, digits, and `_`, and must not contain `$`. The `$` character is reserved for compiler-generated internal names. ```javascript // Declare and initialize a variable var user = fetchUser(1) // Declare a variable, which defaults to `none` var report // Declare multiple variables and assign them a single value var x, y = 100 ``` #### Variable Assignment and Value Semantics Use the `=` operator to assign or reassign a variable to a new value. Using `=` on an undeclared variable will cause a compile-time error. ```javascript var name = "Alice" name = "Bob" // OK: Re-assigning a declared variable // Re-assign multiple existing variables at once x, y = 200 // OK, if x and y were previously declared // ERROR: 'username' was never declared with 'var' username = "Charlie" ``` **Object and Array Composition** You can compose new objects and arrays directly in assignments by using object and array literals. This is the normal way to build up a fresh value from existing variables and expressions. ```javascript var fullName = user.firstName + " " + user.lastName var profile = { id: user.id, name: fullName, active: true } var summary = [user.id, fullName, role] ``` Object literal keys are always explicit. CascadaScript does not support JavaScript object-property shorthand, so write `{ name: name }`, not `{ name }`. This works especially well when you want to create a new value instead of mutating an existing one. **Assignment creates an independent copy.** Objects and arrays are deep copied, not shared by reference. ```javascript var a = {x: 1, y: 2} var b = a // b receives a deep copy a.x = 10 b.x // 1 - b is independent var nums = [1, 2, 3] var copy = nums // copy receives a deep copy nums[0] = 99 copy[0] // 1 - copy is independent ``` This ensures concurrent operations never interfere-each variable owns its data independently. **Performance Note** Cascada uses optimized techniques so that assignments do not copy entire objects. Objects may be shared internally until modified, at which point only the affected parts are copied as needed. This keeps memory usage and performance overhead low while preserving the simple independent value semantics shown in the examples. **Property Assignment** You can directly assign to object properties and array elements: ```javascript var point = {x: 1, y: 2} point.x = 10 var items = [1, 2, 3] items[0] = 100 ``` When you assign an async value to a property, code that reads that property waits for the value to resolve: ```javascript var point = {x: 1, y: 2} point.x = slowApiCall() return { x: point.x, // waits because this value is being read y: point.y // no wait needed - point.y is already resolved } ``` **Note:** Property Assignment is a script-only feature and is not available in the Cascada template language. #### Mutation Methods and Side Effects Direct assignment (`=` and property `=`) is the safe, idiomatic way to update values in Cascada. Mutation methods - methods that modify an existing value in-place rather than producing a new one - need more care. The main unsafe cases are the familiar JavaScript array mutators: `.push()`, `.pop()`, `.shift()`, `.unshift()`, `.splice()`, `.sort()`, `.reverse()`, `.fill()`, and `.copyWithin()`. Treat similar in-place methods on custom objects the same way: they are side effects in exactly the same sense as writing to a database or calling a stateful external service. The problem is not "methods are always forbidden". The problem is concurrent mutation of the same value. If only one execution path is mutating a local value, ordinary JavaScript methods like `items.push(x)` are fine. But when concurrent branches can touch the same `var`, these methods become race-prone. Calling a mutation method on a plain `var` inside a concurrent `for` loop is unsafe - iterations run concurrently, so whichever branch finishes last wins and source-code order is not preserved: ```javascript // UNSAFE - concurrent iterations race on the same var var items = [] for id in ids items.push(fetchItem(id)) // order not guaranteed endfor ``` If you truly do not care about preserving source order, a plain mutable `var` may still be acceptable in non-concurrent code paths. But when multiple concurrent branches build a collection, do not mutate one shared `var` from all branches. Use one of these ordering tools instead: - Use a `data` channel when concurrent branches are assembling an array or object and the final value should be deterministic. - Use an `each` loop when every iteration must finish before the next one starts. - Use the `!` operator when the thing being mutated is a stateful object from the render context, such as a database handle, queue, file writer, or other external service. **`data` channel (preferred for building collections)** - this is the main mitigation. Writes run concurrently, but the assembled result always matches source-code order: ```javascript data result for id in ids var item = fetchItem(id) result.items.push(item) endfor return result.snapshot() ``` Use the `data` channel when you are assembling arrays or objects from concurrent code, whether order matters strictly or you just want to avoid shared-mutation races entirely. **`each` loop (sequential iteration)** - runs one iteration at a time, making mutation methods on a plain `var` safe: ```javascript var items = [] each id in ids items.push(fetchItem(id)) // safe: each iteration completes before the next starts endeach ``` **`!` operator (for context objects)** - serializes calls on an object from the script context; see [Sequential Execution with `!`](#sequential-execution-with-): ```javascript // 'collection' is a context object for id in ids collection!.push(fetchItem(id)) // sequential; rest of the loop still runs concurrently endfor ``` **Scoping: No Shadowing of Visible Names** You cannot declare a variable in an inner scope (e.g., inside a `for` loop or `if` block) if a variable with the same name is already declared in an outer scope. This prevents accidental overwrites in concurrent execution. ```javascript var item = "parent" for i in range(2) // ERROR: 'item' is already declared in the outer scope. var item = "child " + i endfor ``` Functions create clean scopes: their parameters and body cannot see outer local declarations, so they may freely reuse outer names. ```javascript var item = "parent" function render(item) return item // OK: function scope is clean endfunction ``` Variables declared inside control-flow blocks (`if`, `for`, `switch`, etc.) are local to that block and are not visible outside it. ```javascript if condition var local = "only visible here" endif // ERROR: 'local' is not defined here ``` To use a value both inside and outside a block, declare it in the outer scope first: ```javascript var status = "default" if condition status = "updated" // assigns to the outer variable endif // 'status' is visible and possibly updated here ``` **Handling `none` (null)** The keyword `none` represents `null` in CascadaScript. Accessing a property on `none` produces an `Error Value`, so any dependent expression or assignment becomes poisoned. Variables declared without an initial value default to `none`: ```javascript var report // defaults to none (null) var title = report.title // title becomes an Error Value return title // returning it makes the script fail ``` Accessing a missing property on a scalar primitive such as a number or boolean also produces an `Error Value`. Optional object, array, and string reads remain lenient: ```javascript var bad = (5).missing // Error Value var ok1 = obj.missing // undefined var ok2 = items[10] // undefined var ok3 = "abc"[9] // undefined ``` ### The Context Object The context is the plain JavaScript object you pass when running a script. It is how you inject external data, functions, and services into Cascada: ```javascript const result = await env.renderScriptString(script, { userId: 123, fetchUser: (id) => db.users.findById(id), db: myDatabase }); ``` Inside the script, context properties are accessed by name just like any other variable: ```javascript var user = fetchUser(userId) return user.name ``` **Context values are read-only unless you first copy them into a local `var`:** you cannot modify a context path directly in script code. When you assign a context property to a `var`, you get an independent copy, so later changes to that variable do not affect the original context object. ```javascript appConfig.debug = true // ERROR: cannot modify context directly var config = appConfig // local copy config.debug = true // appConfig is unchanged ``` ### Literals, Operators, and Expressions CascadaScript supports a wide range of expressions, similar to JavaScript. #### Literals You can use standard literals for common data types: * **Strings**: `"Hello"`, `'World'` * **Numbers**: `42`, `3.14159` * **Arrays**: `[1, "apple", true]` * **Dicts (Objects)**: `{ key: "value", "another-key": 100 }`; keys must be explicit (`{ name: name }`, not `{ name }`) * **Booleans**: `true`, `false` #### Math All standard mathematical operators are available: `+` (addition), `-` (subtraction), `*` (multiplication), `/` (division), `//` (integer division), `%` (remainder), `**` (power). Operands must be numeric in scripts, except that `+` also supports `string + string`. Mixed coercions such as `"5" + 3`, `"x" + null`, and `"5" * 2` produce an Error Value of [kind](#the-kind-property) [`IncompatibleOperands`](#the-kind-property); use `~` for explicit text concatenation and `| int` / `| float` for numeric conversion. Floating-point division by zero follows JavaScript (`5 / 0` is `Infinity`), while `5 % 0` produces [`NaNResult`](#the-kind-property) and BigInt division or modulo by zero produces [`DivideByZero`](#the-kind-property). ```javascript var price = (item.cost + shipping) * 1.05 ``` #### Comparisons and Logic Standard comparison (`==`, `!=`, `===`, `!==`, `>`, `>=`, `<`, `<=`) and logic (`and`, `or`, `not`) operators are used for conditional logic. In scripts, `==` and `!=` are strict (`===` / `!==`), ordering compares only number-with-number or string-with-string, and `in` requires a collection. ```javascript if (user.role == "Admin" and not user.isSuspended) or user.isOwner // ... grant access endif ``` Cascada also provides a rich, data-centric error handling model. You can test if a variable contains a failure using the `is error` test. For more details, see [Error Handling](#error-handling). #### Inline `if` Expressions For concise conditional assignments, you can use an inline `if` expression. This uses the Python-style conditional-expression syntax rather than the JavaScript `condition ? a : b` form. ```javascript // Syntax: value_if_true if condition else value_if_false var theme = "dark" if user.darkMode else "light" ``` #### Regular Expressions You can create regular expressions by prefixing the expression with `r`. ```javascript var emailRegex = r/^[^\s@]+@[^\s@]+\.[^\s@]+$/ if emailRegex.test(user.email) // Valid email address. endif ``` ### Filters and Global Functions CascadaScript supports the full range of Nunjucks [built-in filters](https://mozilla.github.io/nunjucks/templating.html#builtin-filters) and [global functions](https://mozilla.github.io/nunjucks/templating.html#global-functions). #### Filters Filters are applied with the pipe `|` operator. ```javascript var title = "a tale of two cities" | title var users = ["Alice", "Bob"] return { title: title, // "A Tale Of Two Cities" users: users | join(", ") // "Alice, Bob" } ``` #### Global Functions Global functions like `range` can be called directly. ```javascript // range(n) returns [0, 1, ..., n-1] for i in range(3) processItem(i) // called with i = 0, 1, 2 endfor ``` #### Additional Global Functions ##### `cycler(...items)` The `cycler` function creates an object that cycles through a set of values each time its `next()` method is called. ```javascript // cycler requires sequential order - use 'each' so calls to next() stay in order data rows = [] var rowClass = cycler("even", "odd") each item in items // First item gets "even", second "odd", third "even", etc. rows.push({ class: rowClass.next(), value: item }) endeach return rows.snapshot() ``` ##### `joiner([separator])` The `joiner` creates a function that returns the separator (default is `,`) on every call except the first. This is useful for delimiting items in a list. ```javascript var comma = joiner(", ") var output = "" each tag in ["rock", "pop", "jazz"] output = output + comma() + tag endeach // output is "rock, pop, jazz" ``` ## Control Flow This section covers control flow constructs. Remember that Cascada's concurrent-by-default execution means loops and conditionals behave differently than in traditional languages. These constructs also participate in Cascada's error-propagation model; for the full rules on poisoning, detection, and recovery, see [Error Handling](#error-handling). ### Conditionals ``` if condition // statements elif anotherCondition // statements else // statements endif ``` ### Switch Statements ```javascript switch expression case value1 // statements case value2 // statements default // statements endswitch ``` Switch statements provide a clean way to handle multiple conditional branches based on a single expression. Each branch creates its own scope, similar to `if` statements. **Example:** ```javascript var orderStatus = order.status var nextStep var notification var trackingUrl switch orderStatus case "pending" nextStep = "process_payment" notification = "awaiting_payment" case "confirmed" nextStep = "prepare_shipment" notification = "order_confirmed" case "shipped" nextStep = "track_delivery" trackingUrl = getTrackingUrl(order.id) default nextStep = "review_order" notification = "unknown_status" endswitch ``` **Important:** Unlike C-style languages, Cascada's `switch` does not have fall-through behavior - each `case` exits automatically without needing a `break`. The `default` branch runs when no `case` matches. ### Loops Cascada provides `for`, `while`, and `each` loops for iterating over collections and performing repeated actions, with powerful built-in support for asynchronous operations. ##### `for` Loops: Iterate Concurrently Use a `for` loop to iterate over arrays, dictionaries (objects), async iterators, and other iterable data structures. By default, the body of the `for` loop executes concurrently for each item, maximizing I/O throughput for independent operations. ```javascript // Each iteration runs concurrently, fetching user details data result for userId in userIds var user = fetchUserDetails(userId) result.users.push(user) // data channel preserves source-code order endfor return result.snapshot() ``` **Concurrency Limits** You can control the maximum number of concurrent iterations using the `of` keyword followed by an expression that evaluates to a number. This allows you to rate-limit API calls or manage resource usage dynamically. ```javascript var limit = 5 // Process items 5 at a time for item in largeCollection of limit processItem(item) endfor ``` You can iterate over various collection types: * **Arrays**: ```javascript data result var items = [{ title: "foo", id: 1 }, { title: "bar", id: 2 }] for item in items result.posts.push({ id: item.id, title: item.title }) endfor return result.snapshot() ``` * **Objects/Dictionaries**: Iterates over keys and values. Note that concurrency limits (`of N`) are ignored for plain objects. ```javascript text log var food = { ketchup: '5 tbsp', mustard: '1 tbsp' } for ingredient, amount in food log("Use ", amount, " of ", ingredient) endfor ``` * **Unpacking Arrays**: ```javascript var points = [[0, 1, 2], [5, 6, 7]] text log for x, y, z in points log("Point: ", x, ", ", y, ", ", z) endfor ``` * **Async Iterators**: Iterate seamlessly over async generators or streams. Cascada automatically handles waiting for items to be yielded. **Context Setup:** ```javascript const context = { generateNumbers: async function* () { yield 1; await new Promise(r => setTimeout(r, 100)); yield 2; } }; ``` **Script:** ```javascript text log for num in generateNumbers() log("Received: ", num) endfor ``` **The `else` block** A `for` loop can have an `else` block that is executed only if the collection is empty: ```javascript text log for item in [] log("Item: ", item.name) else log("The collection was empty.") endfor ``` ##### `while` Loops: Iterate Sequentially based on Condition Use a `while` loop to execute a block of code repeatedly as long as a condition is true. Unlike the concurrent `for` loop, the `while` loop's body executes sequentially. The condition is re-evaluated only after the body has fully completed its execution for the current iteration. ``` while some_expression // These statements run sequentially in each iteration endwhile ``` ##### `each` Loops: Iterate Sequentially For cases where you need to iterate over a collection but preserve strict sequential order, use an `each` loop. It has the same syntax as a `for` loop but guarantees that each iteration completes before the next one begins. ``` each item in collection // Each iteration completes before the next one starts endeach ``` ##### The `loop` Variable Inside a `for`, `while`, or `each` loop, you have access to the special `loop` variable, which provides information about the current iteration. **Always-Available Properties** These properties are available in all loop types and modes: * `loop.index`: The current iteration of the loop (1-indexed). * `loop.index0`: The current iteration of the loop (0-indexed). * `loop.first`: `true` if this is the first iteration. **Length-Dependent Properties** Properties that require knowledge of the total collection size: * `loop.length`: The total number of items in the sequence. * `loop.last`: `true` if this is the last iteration. * `loop.revindex`: The number of iterations until the end (1-indexed). * `loop.revindex0`: The number of iterations until the end (0-indexed). Use the following guidelines to determine if these properties are available: 1. Arrays and Objects: **Always Available.** Because the size of an array or object is known upfront, these properties are available regardless of whether the loop runs concurrently, sequentially, or with a concurrency limit. 2. Concurrent Async Iterators: **Available (Async).** For fully concurrent async iterators, `loop.length` and `loop.last` are resolved asynchronously after Cascada has consumed the entire iterator. In practice, these behave like promise-backed loop metadata: loop bodies can start immediately as items arrive, and expressions that depend on `loop.length` or `loop.last` simply wait until the stream has been fully consumed. 3. Sequential or Constrained Async Iterators: **Not Available.** When an async iterator is restricted - by `each` or by a concurrency limit (`of N`) - Cascada treats it as a stream and does not provide `loop.length` or `loop.last`. In these modes, the loop only learns about the next item by continuing iteration. If an iteration were allowed to wait on `loop.length` or `loop.last`, it could block the very iteration progress needed to discover the end of the stream, causing a deadlock. In other words: - In an `each` loop, the current iteration must finish before Cascada can request the next item. Waiting for `loop.length` or `loop.last` would therefore wait for the end of the stream while preventing the stream from advancing. - In a bounded `for ... of N` loop, worker slots move on independently, and earlier iterations may still be unfinished while later items are being fetched. If all active workers waited for `loop.length` or `loop.last`, no worker would be free to keep draining the iterator, so the end would never be discovered. Because of that, these properties are intentionally treated as unavailable rather than as deferred values in sequential or bounded async-iterator loops. 4. `while` Loops: **Not Available.** Since a `while` loop runs until a condition changes, the total number of iterations is never known before all iterations complete. ### Error handling and recovery with conditionals and loops When an Error Value affects a conditional or loop, Cascada ensures that corrupted data never silently produces incorrect results by propagating the error to any variables or channels that would have been modified. #### Error handling with `if` and `switch` statements If the condition of an `if` statement (or the expression of a `switch` statement) evaluates to an Error Value, all branches are skipped, and the error is propagated to any variables or channels that would have been modified within any branch. ```javascript var user = fetchUser(userId) // May fail var accessLevel // declare in outer scope // If user is an Error Value, both branches are skipped // and accessLevel becomes poisoned (it would have been modified) if user.role == "admin" accessLevel = "full" else accessLevel = "limited" endif // If user was an error, accessLevel is now poisoned ``` This behavior is important to understand: it's not just that the code doesn't execute - any variables or channels that would have been assigned in any of the branches become poisoned. This ensures you can detect downstream that something went wrong, rather than having undefined or stale values. **Note:** `switch` statements behave identically - if the switch expression is an Error Value, all `case` and `default` branches are skipped and their outputs become poisoned. #### Error handling with loops If a loop's iterable evaluates to an Error Value, the loop body is skipped and the error propagates to any variables or channels that would have been modified by the loop. ```javascript var posts = fetchPosts() // May fail data out // If posts is an Error Value, loop body is skipped // and out becomes poisoned for post in posts out.titles.push(post.title) endfor // If posts was an error, out is now poisoned ``` Similar to conditionals, the loop doesn't just skip execution - any outputs or variables that the loop body would have modified become poisoned, ensuring error detection downstream. In scripts, iterating a scalar primitive such as a number or boolean also produces an `Error Value`. Iterating `none` still runs the `else` branch because it represents an absent collection. For details on detecting and recovering from errors in your scripts, see the [Error Handling](#error-handling) section. ## Channels Channels are named values you build over time. You write into them with assignments and method calls, and read the current assembled value with `snapshot()`. They are the main tool for ordered writes in CascadaScript: their writes run as soon as their inputs are ready, and the final assembled result still follows source-code order. Channels also participate in Cascada's error-propagation model; for the full rules on poisoning, detection, and recovery, see [Error Handling](#error-handling). | Declaration | Type | Purpose | |---|---|---| | `text name` | Text channel | Build a text string | | `data name` | Data channel | Build structured objects and arrays | Use `name.snapshot()` to read the current assembled value. `snapshot()` is an observable operation - it waits for any pending writes to finish before returning. Because of that, it is more expensive than reading a plain `var`, so prefer `var` for simple cases and reach for `data` or `text` when you need ordered assembly. ### A Simple Example Before diving into the details, here's a simple `text` channel example:
CascadaScript ```javascript text log log("Starting import\n") for user in users log("Imported: ", user.name, "\n") endfor log("Done.") return log.snapshot() ```
Final Text ```text Starting import Imported: Alice Imported: Bob Done. ```
### How Channel Writes Are Ordered Channel writes execute as soon as their required input data is available, following the same data-driven scheduling as the rest of Cascada. The key guarantee is that the assembled result is always in source-code order, regardless of when individual writes actually execute. ### The `text` Channel: Generating Text The `text` channel builds a string of text. It is the simplest channel to reach for when concurrent code needs to contribute to one final piece of text while preserving source-code order. ```javascript text log log("Processing user ", userId, "...") for item in items log("Item: ", item.name) endfor log("...done.") return log.snapshot() ``` Two write forms: | Syntax | Description | |---|---| | `name(expr, ...)` | Appends all arguments to the text stream in order; `name()` writes nothing | | `name = expr` | Overwrites the entire text with `expr` | ### The `data` Channel: Building Structured Data The `data` channel is the main tool for constructing structured output. It is especially useful when concurrent code needs to build arrays or objects in a predictable order - all writes execute concurrently, but the assembled result always matches source-code order. This is the right alternative to [mutation methods on plain `var` values](#mutation-methods-and-side-effects), which race in concurrent code. The key difference from a plain `var` is that `data` operations such as `.push()`, `.merge()`, and `.append()` are channel commands, not ordinary JavaScript in-place mutations. They are scheduled and assembled safely by Cascada, so they remain safe even when multiple concurrent branches write to the same `data` channel. On a plain `var`, those same method names are just standard JavaScript side effects on the current value, so concurrent calls do not get ordered assembly guarantees. Use a plain `var` when you are building a value locally in one place, or when you genuinely do not need channel ordering/assembly behavior. Use a `data` channel when multiple concurrent branches contribute to the same result, or when you want ordered path-based construction without shared-mutation races. As a rule of thumb, `data` channels optimize for correctness and ordered assembly, not raw in-memory mutation speed. On very large nested structures, many fine-grained property writes can be slower than composing a plain object or array locally and assigning or returning it once. Here's a simple example:
CascadaScript ```javascript data out // Set a simple value out.user.name = "Alice" // Initialize 'logins' and increment it out.user.logins = 0 out.user.logins++ // The 'roles' array is created // automatically on first push out.user.roles.push("editor") return out.snapshot() ```
Final Assembled Data ```json { "user": { "name": "Alice", "logins": 1, "roles": [ "editor" ] } } ```
#### Implicit Initialization in `data` The `data` channel automatically initializes structural values when assembling output. This allows data to be built declaratively without manual setup. ###### What `data` Initializes Automatically * **Objects (`{}`)** Created on first property write or object operation (`merge`, `deepMerge`). ```cascada out.user.name = "Alice" out.settings.merge({ theme: "dark" }) ``` * **Arrays (`[]`)** Created on first array operation. ```cascada out.items.push("a") ``` * **Strings (`""`) - string operations only** Created on first string-specific operation. ```cascada out.log.append("Started\n") out.title += "!" ``` ###### What `data` Does *Not* Initialize Scalar values must be explicitly initialized before use: * **Numbers** ```cascada out.count = 0 out.count++ ``` * **Booleans / logical values** ```cascada out.ready = false out.ready ||= true ``` ###### Summary | Type | Auto-initialized | Notes | | ------- | ---------------- | ---------------------- | | Object | Yes | Structural operations | | Array | Yes | Structural operations | | String | Yes | String operations only | | Number | No | Must initialize | | Boolean | No | Must initialize | #### `data` Operations Below is a detailed list of all available commands and operators. **Assignment and Deletion** | Command | Description | |---|---| | `name.path = value` | **Replaces** the value at `path`. Creates objects/arrays as needed. Shorthand for `set`. | | `name.path.delete()` | **Deletes** the value at `path`. | **Array Operations** | Command | Description | |---|---| | `name.path.push(value)` | Appends an element to the array at `path`. | | `name.path.concat(value)` | Concatenates another array or value to the array at `path`. | | `name.path.pop()` | Removes the last element from the array at `path`. | | `name.path.shift()` | Removes the first element from the array at `path`. | | `name.path.unshift(value)`| Adds one or more elements to the beginning of the array at `path`. | | `name.path.reverse()` | Reverses the order of the elements in-place. | | `name.path.at(index)` | Replaces `path` with the element at the specified `index`. | | `name.path.sort()` | Sorts the array at `path` in-place. | | `name.path.sortWith(func)` | Sorts the array using a custom comparison function. | | `name.path.arraySlice(start, [end])`| Replaces `path` with a slice of the array. | **Object Manipulation** | Command | Description | |---|---| | `name.path.merge(value)` | Merges the properties of an object into the object at `path`. Shallow merge. | | `name.path.deepMerge(value)`| Deeply merges the properties of an object into the object at `path`. | **Arithmetic Operations** These operators require the target to be a number (must be initialized first). | Command | Description | |---|---| | `name.path += value` | Adds a number to the target. | | `name.path -= value` | Subtracts a number from the target. | | `name.path *= value` | Multiplies the target by a number. | | `name.path /= value` | Divides the target by a number. | | `name.path++` | Increments the target number by 1. | | `name.path--` | Decrements the target number by 1. | | `name.path.min(value)` | Replaces target with `min(target, value)`. | | `name.path.max(value)` | Replaces target with `max(target, value)`. | **String Operations** String is created automatically if the path does not exist. | Command | Description | |---|---| | `name.path += value` | Appends a string to the target. | | `name.path.append(value)`| Appends a string to the string value at `path`. | | `name.path.toUpperCase()` | Replaces with uppercase version. | | `name.path.toLowerCase()` | Replaces with lowercase version. | | `name.path.slice(start, [end])` | Replaces with the extracted section. | | `name.path.substring(start, [end])`| Replaces with the extracted section (no negative indices). | | `name.path.trim()` | Removes whitespace from both ends. | | `name.path.trimStart()` | Removes leading whitespace. | | `name.path.trimEnd()` | Removes trailing whitespace. | | `name.path.replace(find, replace)` | Replaces the first occurrence. | | `name.path.replaceAll(find, replace)` | Replaces all occurrences. | | `name.path.split([separator])` | Replaces with an array of substrings. | | `name.path.charAt(index)` | Replaces with the character at the specified index. | | `name.path.repeat(count)` | Repeats the string `count` times. | **Logical & Bitwise Operations** | Command | Description | |---|---| | `name.path &&= value` | Logical AND assignment. | | `name.path \|\|= value` | Logical OR assignment. | | `name.path &= value` | Bitwise AND assignment. | | `name.path \|= value` | Bitwise OR assignment. | | `name.path.not()` | Logical NOT. | | `name.path.bitNot()` | Bitwise NOT. | #### Advanced Pathing Paths in `data` commands are highly flexible. * **Dynamic Paths**: Paths can include variables and expressions. ```javascript for user in userList result.report.users[user.id].status = "processed" endfor ``` * **Root-Level Modification**: Use the `data` value directly to replace the root. ```javascript // Replaces the entire data object with a new one result = { status: "complete", timestamp: now() } ``` After re-assignment, you can use methods appropriate for the new type: ```javascript result = [] result.push("first item") ``` * **Array Index Targeting**: Target specific array indices with square brackets. The empty bracket notation `[]` refers to the last item added in the script's sequential order. ```javascript result.users[0].permissions.push("read") result.users.push({ name: "Charlie" }) result.users[].permissions.push("read") // Affects "Charlie" ``` #### Handling Missing and `none` Targets * **Structure-building methods** (`.push()`, `.merge()`, `.append()`) can create the needed structure when the target path does not exist yet. * **Arithmetic and logical operators** (`+=`, `--`, `&&=`, etc.) throw a runtime error if the target is `none`/`null` or missing. Initialize explicitly first. #### Extending `data` with Custom Methods You can add your own custom methods or override existing ones for the built-in `data` channel using `env.addDataMethods()`. This lets you extend `data` with domain-specific operations while keeping the same ordered channel semantics. ```javascript // In your JS setup env.addDataMethods({ // methodName is how you'll call it in the script: name.path.methodName(...) methodName: function(target, ...args) { // ... your logic ... return newValue; } }); ``` **Parameters:** * `target`: The current value at the path the command is targeting. If the path doesn't exist yet, `target` will be `undefined`. * `...args`: A list of the arguments passed to the method in the script. **Return Value:** * **If you return any value**, it replaces the `target` value at that path. * **If you return `undefined`**, it signals the engine to delete the property at that path. **Overriding Operators:** All shortcut operators (`+=`, `++`, `&&=`, etc.) are mapped to underlying methods. | Operator | Corresponding Method | |---|---| | `name.path = value` | `set(target, value)` | | `name.path += value` | `add(target, value)` | | `name.path -= value` | `subtract(target, value)` | | `name.path *= value` | `multiply(target, value)` | | `name.path /= value` | `divide(target, value)` | | `name.path++` | `increment(target)` | | `name.path--` | `decrement(target)` | | `name.path &&= value` | `and(target, value)` | | `name.path \|\|= value` | `or(target, value)` | | `name.path &= value` | `bitAnd(target, value)` | | `name.path \|= value` | `bitOr(target, value)` | **Example: Adding a custom `upsert` method** ```javascript // --- In your JavaScript setup --- env.addDataMethods({ upsert: (target, newItem) => { if (!Array.isArray(target)) { target = []; } const index = target.findIndex(item => item.id === newItem.id); if (index > -1) { Object.assign(target[index], newItem); } else { target.push(newItem); } return target; } }); // --- In your CascadaScript --- data out out.users.upsert({ id: 1, name: "Alice" }) out.users.upsert({ id: 1, name: "Alice", status: "active" }) return out.snapshot() ``` ### Error handling and recovery with channels When an Error Value is written to a channel, that channel becomes poisoned. This means the channel's final output will be an Error Value, which causes the current script's `snapshot()` or `return` to fail. ```javascript data out var user = fetchUser(userId) // May fail // If user is an Error Value, this write poisons out out.userName = user.name // out is now poisoned - returning it will fail the script return out.snapshot() ``` You can protect these values from poisoning and recover from errors using `guard` blocks: ```javascript data out guard var payload = fetchData() // May fail out.result = payload recover err out.result = "fallback value" endguard return out.snapshot() ``` For details, see [Protecting State with `guard`](#protecting-state-with-guard). ## Sequencing External Interactions Cascada can order its own internal work automatically, including dependencies and data/text channel assembly. The hard case is imported native functions and objects from the render context: APIs, mutable object methods, database handles, file writers, LLM clients, and helpers that read or change shared state. Cascada cannot know whether those calls are pure or side-effectful, so you mark the ordering explicitly. There are two dedicated sequencing constructs for external interactions: | Construct | Syntax | Purpose | |---|---|---| | `!` marker | `obj!.method()`, `obj!.prop` | Sequence calls and reads on a static context path | | `sequence` object | `sequence db = services.db`, `var user = db.getUser(1)` | Create a named ordered interface around one external object | Both are narrow by design: they sequence only the object or path they touch, while unrelated work in the script continues concurrently. ### Sequential Execution with `!` External objects in the context — databases, APIs, stateful services — often have operations with side effects. Because Cascada runs independent operations concurrently, calling them without coordination can produce unpredictable results. Use `!` to mark a call as having side effects on a path. Once any call on a path is marked with `!`, that path becomes sequential — all subsequent accesses wait for the preceding operation to complete, whether they carry `!` or not, including property reads, while unrelated operations continue concurrently. Behind the scenes, each sequenced access awaits the promise returned by the previous operation on that path before starting, so the full async operation completes before the next begins. Sequencing is hierarchical: a side effect declared on a parent path sequences all sub-paths beneath it. Marking `bank!` means everything that follows under `bank` — `bank.account`, `bank.user`, and so on — must wait for that operation to complete. Sequential paths also participate in Cascada's error-propagation model; for the full rules on poisoning, repair, and recovery, see [Error Handling](#error-handling). ```javascript // `!` on deposit() signals side effects — bank.account becomes a sequential path. bank.account!.deposit(100) bank.account.getStatus() // waits — plain call on a sequential path bank.account!.withdraw(50) // waits — ! calls also wait and extend the sequence var bal = bank.account.balance // waits — property reads are sequenced as well ``` Sequencing a parent path affects all sub-paths: ```javascript // `!` on bank signals side effects on the bank object as a whole. bank!.resetUser(userInfo) bank.account.deposit(100) // waits — bank.account is under bank bank.user.getName() // waits — bank.user is under bank too ``` For details on how to handle errors within a sequential path, see [Repairing Sequential Paths with `!!`](#repairing-sequential-paths-with-) in the Errors Are Data section. #### Method-Specific Sequencing You can also sequence calls to a specific method on an object, rather than making the whole path sequential. Place the `!` after the method name: ```javascript // Only calls to 'log' are sequential logger.log!("Entry 1") logger.log!("Entry 2") // Unmarked methods run concurrently logger.getStatus() ``` This is useful for rate-limiting or ordering specific actions (like "append") while keeping the rest of the object non-blocking. Note that unlike path sequencing (`obj!.method()`), unmarked calls to the same method (`logger.log()`) will not wait on the method-specific sequence. #### Ordered External APIs Use sequential paths for stateful external APIs that need strict ordering. For example, a turtle graphics object can be provided in the render context, and each drawing command can be ordered with `!`: ```javascript // `turtle` is provided by the render context. turtle!.penDown() turtle!.moveTo(10, 10) turtle!.lineTo(50, 10) turtle!.lineTo(50, 40) turtle!.penUp() ``` Only the `turtle` path is serialized. Other independent work in the script can still run concurrently. `!` guarantees **order** and that the call **runs** - but not that it has *finished* when the render resolves. The render waits only on the returned value, not on a pure side effect, so a slow `db!.save(record)` may settle after the render returns. If you need it awaited, fold its result into what you return (`result.ack = db!.save(record)`). #### Context Requirement for Sequential Paths Sequential paths must reference objects from the context, not local variables. If the root name is absent from the context, the path is an Error Value of [kind](#the-kind-property) [`UnknownVariable`](#the-kind-property). The JS context object: ```javascript // Assuming 'db' is provided in the context object: const context = { db: connectToDatabase() }; ``` The script: ```javascript // CORRECT: Direct reference to context property db!.insert(data) // WRONG: Local variable copy var database = db database!.insert(data) // Error: sequential paths must be from context ``` Nested access from context properties works fine: ```javascript services.database!.insert(data) // CORRECT (if 'services' is in context) ``` **Why this restriction?** The engine uses object identity from the context to guarantee sequential ordering. Copying context objects to local variables breaks this tracking, which is why it's not allowed. Support for using `!` through `function` parameters is planned, but it is not implemented yet. ### The `sequence` Construct A `sequence` declaration creates a sequence object that wraps an external object from the render context with strictly sequential access. Use it for imported native objects whose methods or property reads depend on order, such as mutable API clients, database transactions, graphics contexts, file writers, state machines, or helpers that touch shared state. Every read and call through the sequence object is serialized in source-code order. ```javascript sequence db = services.db var user = db.getUser(1) var state = db.connectionState return { user: user, state: state } ``` ```javascript sequence db = services.db var id = db.api.client.getId() return id ``` **Key characteristics:** - The initializer must come from the context object - Supports value-returning calls: `var x = seq.method(args)` - Supports property reads: `var s = seq.status` - Supports nested sub-path calls: `var id = seq.api.client.getId()` - Supports `snapshot()`: `var snap = seq.snapshot()` - Property assignment is currently a compile error, but this is expected to be supported in the future ```javascript sequence db = services.db db.connectionState = "offline" // compile error - assignment not allowed ``` If a `sequence` becomes poisoned, the built-in way to recover it is with a `guard`. See [Protecting State with `guard`](#protecting-state-with-guard). ### The `sequence` Construct vs. `!` `sequence` and `!` both give you ordering, but they solve different problems: | | `!` marker | `sequence` | |---|---|---| | **What it is** | A marker on a static context path | A declared sequence object | | **What it is for** | Ordering side effects on one external context path | Ordered reads and calls on one external context object | | **Return values** | Mainly used for side-effectful operations | Read immediately in normal expressions | | **Example** | `db!.insert(user)` | `var user = db.getUser(1)` | Use `!` when you want to serialize side effects on a specific context path without wrapping the whole object. Use `sequence` when the external object itself is your ordered interface. ## Functions and Reusable Components Functions in CascadaScript are declared with `function ... endfunction`. They let you define reusable chunks of logic that build and return values. They operate in a completely isolated scope and are the primary way to create modular, reusable components in CascadaScript. These functions use `return` to return values. If no `return` runs, the function returns `none`. Channels declared inside a function are local to that function. ### Defining and Calling a Function A function can call async functions and use `return` to provide its result. Like a script, it runs to completion before its return value is available to the caller.
CascadaScript ```javascript function buildDepartment(deptId) // These two async calls run concurrently. var manager = fetchManager(deptId) var team = fetchTeamMembers(deptId) return { manager: manager.name, teamSize: team.length } endfunction // Call the function. 'salesDept' is the returned object. var salesDept = buildDepartment("sales") return { company: { sales: salesDept } } ```
Final Return Value ```json { "company": { "sales": { "manager": "David", "teamSize": 15 } } } ```
### Keyword Arguments Functions support keyword arguments, allowing for more explicit and flexible calls. You can define default values for arguments, and callers can pass arguments by name. ```javascript // Function with default arguments function input(name, value="", type="text") return { name: name, value: value, type: type } endfunction // Calling with mixed and keyword arguments var passwordField = input("pass", type="password") return passwordField // { name: "pass", value: "", type: "password" } ``` ### Function Handles Functions are values in CascadaScript when they stay inside Cascada-controlled code. You can assign a function handle to a variable, store it in an object or array, return it from another function, and pass it directly to a statically known Cascada function: ```javascript function format(value) return "[" + value + "]" endfunction function apply(fn, value) return fn(value) endfunction function chooseFormatter() return format endfunction var direct = format var tools = { format: format } var list = [format] return [ direct("a"), tools.format("b"), list[0]("c"), chooseFormatter()("d"), apply(format, "e") ] ``` Direct function handles cannot be returned from the script root, passed directly to native or context functions, or used as ordinary scalar values: ```javascript return format // error: root return return { fn: format } // error: root return contains a direct handle return nativeApply(format) // error: native/context call argument return "fn=" + format // error: scalar value use ``` This validation only checks direct uses. It does not follow aliases, so do not use aliases to work around the rule; that is outside the supported function-handle pattern. The same rules apply to imported script functions once they are used as statically known Cascada callables. ### Returning a Computed Value Functions can `return` any ordinary value directly - a primitive, an object literal, or a variable. Channels themselves are not returned directly; use `snapshot()` and return the resulting value: ```javascript function computeTotal(items) var sum = 0 for item in items sum = sum + item.price endfor return sum endfunction var total = computeTotal([ { price: 10 }, { price: 20 }, { price: 30 } ]) return total // 60 ``` ### Dynamic Call Blocks (`call`) A `call` block lets you pass a chunk of code to a function as a callback. The function controls when and how that code executes by calling `caller()` with explicit arguments. #### Syntax In scripts, `call` blocks must be used in assignment form: ```javascript var x = call functionName(args) (param1, param2) // Declare parameters // Block body - use return to provide the value return someValue endcall ``` Or the assignment form without initialization: ```javascript x = call functionName(args) // ... endcall ``` Bare `call` blocks (without assignment) are not supported in scripts. The function invokes the callback by passing arguments: ```javascript function functionName(args) var result = caller(value1, value2) endfunction ``` If no parameters are needed, the `()` can be omitted from the call block. #### Example: Grid Generator ```javascript function grid(rows, cols) data cells = [] for y in range(rows) for x in range(cols) var cell = caller(x, y) // Pass coordinates cells.push(cell) endfor endfor return cells.snapshot() endfunction var gridResult = call grid(3, 3) (x, y) return { position: [x, y], value: x * 10 + y } endcall return gridResult ``` #### Example: Simple Value Transformation ```javascript function sum(items) var total = 0 for item in items var value = caller(item) total = total + value endfor return total endfunction var result = call sum([{price: 10}, {price: 20}, {price: 30}]) (item) return item.price endcall return result // 60 ``` #### Example: Error Handling ```javascript function withRetry(maxAttempts) var attempts = 0 var result = none while attempts < maxAttempts and result is none result = caller() if result is error result = none attempts = attempts + 1 endif endwhile return result endfunction var userData = call withRetry(3) var user = fetchUser(userId) return user endcall return userData ``` #### Variable Scope The call block runs with access to variables from where it was written, not the function's internal scope: ```javascript function processItem(transformer) var internalVar = "function scope" var result = caller(transformer) return result endfunction var outerVar = "call scope" var processed = call processItem(item) (item) // Can access outerVar; Cannot access internalVar return { item: item, context: outerVar } endcall return processed ``` The call block's access to the parent scope is read-only: - **Reads** can see variables from the parent scope (where the call block was written). - **Assignments** to visible parent variables are rejected. - **Fresh `var` declarations** inside the call block stay local to the call block and must not reuse a visible parent name. This ensures the call block remains decoupled from the function's implementation details. #### How Call Blocks Work - **Parameters**: The function explicitly passes values via `caller(args)`, declared as `(params)` in the call block header - **Return value**: The value provided by `return` in the call block body is returned by `caller()` in the function - **Caller's context**: The block reads variables from the scope where it was written, not the function's internal scope - **Execution control**: The function decides when - and how many times - to invoke `caller()` - **Isolated scope**: Writes inside the call block stay local; the function sees only what `caller()` returns ### Error handling and recovery with functions Functions participate in the normal dataflow poisoning rules, but they are still called with poisoned arguments and can handle those Error Values explicitly inside the function body. For comprehensive information on error handling and recovery patterns, see the [Error Handling](#error-handling) section. ## Error Handling Cascada's concurrent-by-default execution creates a unique challenge: when multiple operations run concurrently and one fails, traditional exception-based error handling would need to interrupt the entire execution graph, halting all independent work. Instead, Cascada treats errors as just another type of data that flows through your script. Failed operations produce a special Error Value that is stored in variables, passed to functions, and can be inspected. This data-centric model allows independent operations to continue running while failures are isolated to only the variables and operations that depend on the failed result. ### Error Handling Fundamentals #### Error Handling in Action Here's a concrete example showing how error propagation works in concurrent execution: ```javascript // These three API calls run concurrently var user = fetchUser(123) // succeeds var posts = fetchPosts(123) // fails with network error var comments = fetchComments() // succeeds // Only operations depending on 'posts' are affected var username = user.name // works fine var commentCount = comments.length // works fine var postCount = posts.length // becomes an error var summary = posts + " analysis" // becomes an error // You can detect and repair the error if posts is error postCount = 0 // assign a fallback summary = '' // assign a fallback endif return { username: username, commentCount: commentCount, postCount: postCount, summary: summary } ``` #### The Core Mechanism: Error Propagation Once an Error Value is created, it automatically spreads to any dependent operation or variable - this process is known as error propagation, dataflow poisoning, or just poisoning. This ensures that corrupted data never silently produces incorrect results. #### Data Operations * **Expressions:** If any operand in an expression is an error, the entire expression evaluates to that error. ```javascript var total = myError + 5 // total becomes myError var result = 10 * myError / 2 // result becomes myError ``` An operation that produces `NaN` (such as `0 / 0`) also becomes an Error Value; `Infinity` stays a normal value. * **Function Calls:** If an Error Value is passed as an argument, the function still receives it and can detect or repair it explicitly. ```javascript function processData(value) if value is error return "fallback" endif return value.name endfunction var result = processData(myError) // "fallback" ``` #### Control Flow * **Loops:** A loop whose iterable is an Error Value will not execute its body. The error propagates to all variables and outputs that would have been affected. ```javascript var itemCount = 0 for item in myErrorList itemCount = itemCount + 1 endfor // itemCount is now poisoned ``` * **Conditionals:** If a conditional test evaluates to an Error Value, neither the `if` nor `else` branch executes. The error propagates to all variables modified by either branch. ```javascript if myErrorCondition result = "yes" else result = "no" endif // The 'result' variable is now an Error Value ``` #### Poisoning Channels and Sequencing Constructs * **Channels:** If an Error Value is written to a channel, that channel becomes poisoned, causing the script to fail when the channel is read or returned. * **Sequence Objects:** If a read or call through a `sequence` object fails, that sequence becomes poisoned. Later operations through the same sequence immediately yield an Error Value without executing until the sequence is recovered with `guard`. * **Sequential `!` Paths:** If a call in a sequential execution path (marked with `!`) fails, that path becomes poisoned. Later operations using the same `!path` will instantly yield an Error Value without executing. ```javascript context.database!.connect() // fails context.database!.insert(record) // skipped, returns error immediately context.database!.commit() // skipped, returns error immediately ``` This mechanism ensures that once an operation fails, all dependent results, channels, and sequencing constructs reflect that failure, maintaining data integrity across both concurrent and sequential execution flows. #### Deciding When to Handle Errors **Do not handle errors, let them propagate when:** - The operation is critical to the final output - You want the entire script to fail if this operation fails - The error should bubble up to the calling JavaScript/TypeScript code - There's no reasonable fallback or default value - You're building a strict data pipeline where partial results are unacceptable **Handle errors locally when:** - You have a sensible fallback or default value - The operation is optional or non-critical - You're implementing retry logic for transient failures - You're aggregating results where partial success is acceptable - You want to collect multiple errors for reporting without halting execution - The error represents a business-logic case that should produce specific output (e.g., "user not found" → guest mode) ```javascript // Critical operation - let it propagate and fail the script var primaryData = fetchCriticalData() // Optional enhancement - handle locally var recommendations = fetchRecommendations() if recommendations is error recommendations = [] // Not critical, use empty array as fallback endif return { report: primaryData.summary, recommendations: recommendations } ``` #### How Scripts Fail A script fails only if the value you return is an Error Value. A statement run only for its side effect — a bare call, or `{% do %}` in templates — discards its result, so if it fails the error has no consumer and is dropped. Bind the result if you need to detect it. You can have poisoned values inside the script and still succeed, as long as you repair them or avoid returning them: ```javascript var user = fetchUser(999) // Returns an error if user is error user = { name: "Guest" } // Repaired endif return user.name // Script succeeds: "Guest" ``` If the returned value is still poisoned, the script fails: ```javascript var user = fetchUser(999) // Returns an error return user.name // Script fails ``` #### Errors Thrown by Render Methods JavaScript render methods such as `renderScript(...)`, `renderScriptString(...)`, `renderTemplate(...)`, and `renderTemplateString(...)` reject with Cascada error objects when rendering cannot produce a healthy result: * **`CompileError`**: The script or template could not be compiled. This is a synchronous source error with `lineno`, `colno`, `path`, `label`, `description`, `fullMessage`, and `context`. * **`RuntimeError`**: A fatal runtime failure occurred, such as an invalid runtime contract, inheritance/load failure, or internal structural error. It exposes the same diagnostic fields as other render errors. * **`PoisonError`**: The rendered result depends on one failed operation. * **`PoisonErrorGroup`**: The rendered result depends on multiple failed operations. Its `errors[]` entries are the individual `PoisonError` objects. All render errors expose `message`, `description`, `fullMessage`, and `context`. `PoisonError` and `PoisonErrorGroup` are the same shapes returned by the `#` peek operator. `RuntimeError` is fatal and is not part of dataflow recovery. To distinguish error types in JavaScript `catch` blocks, use `instanceof`. Since `PoisonErrorGroup extends PoisonError`, a single `instanceof PoisonError` check catches both: ```javascript import { PoisonError, CompileError, RuntimeError } from 'cascada-engine'; try { const result = await env.renderScript(script, context); } catch (err) { if (err instanceof PoisonError) { // Catches both PoisonError and PoisonErrorGroup; they share the same interface for (const e of err.errors) { console.error(e.fullMessage); } } else if (err instanceof CompileError) { // Source could not be compiled console.error(err.message); } else if (err instanceof RuntimeError) { // Fatal contract violation throw err; } } ``` ### Detecting and Inspecting Errors #### Detecting and Repairing Errors The fundamental way to detect if a variable holds an Error Value is the `is error` test. Once detected, you can "repair" it by re-assigning the variable. **Example: Assigning a Fallback Value** ```javascript var user = fetchUser(999) // assumed to fail if user is error var msg = user#message // peek at the error details user = { name: "Guest", isDefault: true } endif return user.name // 'Alice' or 'Guest' depending on success ``` **Example: Retrying a Failed Operation** ```javascript var retries = 0 var user var success = false while retries < 3 and not success user = fetchUser(123) if user is not error success = true else retries = retries + 1 endif endwhile if user is error user = { name: "Guest", isDefault: true } endif return user ``` #### Peeking Inside Errors with `#` Because of error propagation, a standard property access like `myError.message` would just return `myError` again. To inspect the properties of an Error Value itself, use the special `#` (peek) operator. This operator "reaches through" the error to access its internal properties without triggering propagation. ```javascript var failedUser = fetchUser(999) if failedUser is error var message = failedUser#message var path = failedUser#errors[0].path var line = failedUser#errors[0].lineno endif ``` **`x#` returns `none` when `x` is not an error.** Always check with `is error` before peeking: ```javascript context.db!.insert(data) // Succeeds // WRONG: Peeking at healthy value returns none, not an error var msg = context.db!#message // none - not useful // CORRECT: Check first, then peek var msg if context.db! is error msg = context.db!#message // Safe endif ``` #### Anatomy of an Error Value Peeking returns the poison error for that value, or `none` when the value is healthy. A single failure returns a `PoisonError`, multiple failures a `PoisonErrorGroup`. Both expose the same interface, so most code handles them uniformly. Access the poison error with `#`. `PoisonError` represents one failed operation. It exposes these fields: * **`description`**: (string) The cause's message text, without the error type prefix, source location, or stack. * **`message`**: (string) Two-line compact diagnostic: the error type and description on the first line, the source location on the second. * **`fullMessage`**: (string) The same first two lines as `message`, plus the Cascada diagnostic stack when available. * **`context`**: (object) The normalized diagnostic context for the failure. May include extra metadata such as `callSignature`, `loop`, or `branch` that appear in the formatted messages. * **`errors`**: (array) A single-item array containing the error itself: `[this]`. * **`name`**: (string) Always `'PoisonError'`. * **`lineno`**: (number) The line number where the error occurred. * **`colno`**: (number) The column number. * **`path`**: (string) The script file where the error originated. * **`label`**: (string) The raw compiler classification token for the source operation (e.g., `'FunCall'`, `'LookupVal'`, `'Divide'`). The formatted `message` renders this as a human-readable phrase such as `call fetchUser(...)`. * **`kind`**: (string) A stable code naming **what** failed, independent of `label` (which names **where**). Useful for inspection, but treat it as diagnostic metadata, not a frozen API — the set may grow. * **`cause`**: (Error) The original JavaScript `Error` object. Always present on `PoisonError`. ##### The `kind` Property The `kind` values below are **strings** carried in the `kind` field — not error classes. When this documentation says a failure "is an `UnknownVariable`" or "produces a `NaNResult`", it means `error.kind === 'UnknownVariable'`; the runtime class is always `PoisonError` / `PoisonErrorGroup`. | `kind` | Failure | |---|---| | `MissingFunction` | called name resolved to `undefined` - no such function/method or context property | | `NotAFunction` | call target is some other type, not a function | | `UserCallThrew` | a called function, filter, data method, or sequence method threw | | `UnknownVariable` | bare variable read names a missing context/global/script symbol | | `NullLookup` | property read on `null`/`undefined` | | `ScalarLookup` | script property read on a scalar primitive with no such property | | `LookupThrew` | a property getter threw | | `IteratorThrew` | a loop iterator or generator threw | | `NotIterable` | script loop source or `in` right-hand operand is not a collection | | `NotDestructurable` | loop element is not array-like for multi-variable destructuring; use `for a, b in [[1, 2]]`, not `for a, b in [1, 2]` | | `InvalidConcurrentLimit` | `of` limit is not a positive number | | `IncompatibleOperands` | script operator operands have incompatible types | | `DivideByZero` | BigInt division or modulo by zero | | `LoadFailed` | a non-fatal `import`/`component`/`include` load failed | | `ImportBindingMissing` | imported name is not exported by the module | | `NaNResult` | a computation produced `NaN` (`Infinity` stays a value) | | `InvalidTextValue` | a value that cannot be converted to text, such as a plain object without text output, a function, or a symbol | | `ContextValueRejected` | a promise supplied by the render context (or returned directly) rejected | The single-item `errors` array is intentional. It lets code process standalone and grouped poison errors the same way: ```javascript var err = value# each item in err.errors log(item.message) endeach ``` `PoisonErrorGroup` represents multiple failures. The aggregate fields override the inherited ones: * **`name`**: (string) Always `'PoisonErrorGroup'`. * **`kind`**: (string) Derived: the shared child `kind` if they all agree, otherwise `'Multiple'`. * **`kinds`**: (array) The sorted unique child `kind`s. * **`totalErrorCount`**: (number) The full number of failures. * **`description`**: (string) A short aggregate description such as `Multiple errors occurred (3)`. * **`message`**: (string) An aggregate intro followed by numbered child `message` values. When the failures exceed the message cap, the header also summarizes the total count and the kinds present. * **`fullMessage`**: (string) The same aggregate intro, with each child's own stack. * **`errors`**: (array) **All** the individual `PoisonError` objects, sorted by source location, each with its own context and cause. The `message` is capped for readability; `errors` is not. The following fields are inherited from `PoisonError` and come from the first child error, so single-error and multi-error handling code can stay the same: * **`cause`** * **`context`** * **`lineno`** * **`colno`** * **`path`** * **`label`** All individual child locations remain available through `errors[]`. `error.message` — compact, two lines: ```text PoisonError: service failed (report.casc) [Line 4, Column 12] call fetchUser (argument names=[userId]) ``` `error.fullMessage` — same header, plus the Cascada execution trace when available: ```text PoisonError: service failed (report.casc) [Line 4, Column 12] call fetchUser (argument names=[userId]) Stack: 1. (report.casc) [Line 3, Column 2] call enrichUser(user) 2. (report.casc) [Line 2, Column 0] For (loop variables=[user]) 3. (report.casc) [Line 1, Column 0] Root (entry name=root) ``` When the first stack frame matches the primary source location, Cascada omits the duplicate and prints it only once. The diagnostic stack is a Cascada execution trace, not just a function-call stack. It can include loops, branches, macros, imports, includes, and other runtime steps when they help explain where the error surfaced. #### Handling Multiple Errors When a value depends on multiple poisoned inputs, their errors are collected into a single aggregate poison error (`PoisonErrorGroup`). The failures may have happened concurrently, sequentially, or simply propagated through different dataflow paths. The group's `.errors[]` entries are individual `PoisonError`s with their original source locations. ```javascript var user = fetchUser(999) // fails var profile = fetchProfile(999) // fails var settings = fetchSettings(999) // fails var summary = user.name + " - " + profile.bio + " - " + settings.theme if summary is error var count = summary#errors | length // 3 data errorList = [] each err in summary#errors errorList.push({ message: err.message, path: err.path, line: err.lineno, column: err.colno, label: err.label }) endeach summary = "User data unavailable" endif ``` This shows every failure that contributed to the poisoned value, not just the first one. ### Advanced Recovery Mechanisms #### Repairing Sequential Paths with `!!` When a sequential path becomes poisoned, the `!!` operator provides two ways to recover: **Repair the Path:** Use `!!` alone to clear the poison state. ```javascript context.db!.insert(data) // Fails and poisons the path context.db!! // Repairs the path context.db!.insert(otherData) // Now executes ``` **Repair and Execute:** Use `!!` before a method call to repair the path and then execute the method. ```javascript context.db!.beginTransaction() context.db!.insert(userData) // Fails, poisons path context.db!.insert(profileData) // Skipped due to poison // Repairs path and executes rollback context.db!!.rollback() ``` This is particularly useful for cleanup operations that must run regardless of failure: ```javascript var file = context.fileSystem!.open(path) context.fileSystem!.writeHeader(metadata) var writeResult = context.fileSystem!.writeData(data) // Might fail // Always close the file, even if writes failed context.fileSystem!!.close() ``` **Checking Path State:** ```javascript context.api!.sendRequest(data) // Might fail if context.api! is error var message = context.api!#message context.api!! // Repair the path endif ``` #### Protecting State with `guard` The `guard` block provides controlled, transaction-like recovery for your script. It allows you to attempt complex operations with the confidence that if something goes wrong, Cascada will automatically restore selected state. You can think of `guard` like a save point: if the block finishes in an error, the specified state is restored before recovery logic runs. #### Syntax ```javascript guard [targets...] // 1. Attempt risky operations // 2. Changes to guarded targets are tracked for recovery recover [err] // Optional recover block; 'err' variable binding is also optional // 3. Runs ONLY if the guard block remains poisoned // 4. Guarded state has already been restored endguard ``` --- #### Default Protection: Guarded State By default, a `guard` block (with no arguments) protects: 1. All channels (`data`, `text`) and sequence objects (`sequence`) Channel writes made inside the block are discarded on error. For sequence objects, this is also the built-in way to recover from poisoning. If the underlying object provides `begin()`, `commit()`, and `rollback()` hooks, `guard` uses them automatically. Missing hooks are tolerated. Hook errors become guard errors. 2. All sequential paths (`!`) If a path such as `db!` becomes poisoned, it is automatically repaired with `!!`. Paths are hierarchical - guarding `api!` also guards `api.db!`, `api.connection!`, etc. **Variables are NOT protected by default.** ##### Example: Database Transaction ```javascript // db! is a sequential path from context db!.beginTransaction() data out guard out.status = "processing" db!.insert(user) db!.update(account) // Assume this fails db!.commit() out.status = "success" recover err // STATE RESTORED: // - data channel writes inside the guard are reverted // - db! is repaired and safe to use db!.rollback() out.error = "Transaction failed: " + err.message endguard return out.snapshot() ``` ##### Example: Guarding a `sequence` Object ```javascript sequence tx = services.tx var state = "starting" guard tx, state state = "running" tx.step("A") tx.fail() state = "done" recover err state = "rolled back" endguard ``` --- #### Selective Protection You can explicitly specify what the guard should protect. ```javascript // Protects the out data channel, the db! path, and the 'status' variable guard out, db!, status ``` ##### Selectors | Selector | Meaning | |----------|---------| | `guard` (no selectors) | Global guard: protects all channels, sequence objects, and sequential paths touched inside the block | | `guard *` | Protect everything (all channels, sequence objects, all sequential paths, all visible variables written inside the guard) | | `guard var` | Protect all visible variables written inside the guard | | `guard data` | Protect all `data` channel declarations touched inside the guard | | `guard text` | Protect all `text` channel declarations touched inside the guard | | `guard sequence` | Protect all `sequence` declarations touched inside the guard | | `guard name1, name2` | Protect specific declaration names (channels, sequence objects, or variables) | | `guard lock!` | Protect a specific sequential path (e.g., `db!`) | | `guard !` | Protect all sequential paths touched inside the guard | > **Rules:** > - `*` cannot be combined with any other selector > - Duplicate selectors are invalid > - The `lock!` and `!` selectors are for sequential paths, not `sequence` objects **Hierarchical Protection of Sequential Paths:** ```javascript guard api! api!.connect() api.db!.insert(data) // Also protected (child of api!) api.connection!.setState(s) // Also protected (child of api!) endguard ``` ##### Example: Protecting Specific Variables or Channel Types ```javascript var attempts = 0 var lastLog = "" data result guard attempts, data attempts = attempts + 1 result.try = attempts lastLog = "Trying..." // not protected riskyOperation() // Fails recover err // 'attempts' is restored to 0 // 'data' channel writes are reverted (all data channels) // 'lastLog' remains "Trying..." (not protected, so not restored) endguard ``` --- #### `guard *` (Protect Everything) ```javascript guard * var x = calculate() var y = fetch() endguard ``` **Performance warning** When variables are protected (via `guard *`, `guard var`, or explicit variable names), any code that depends on such variables must wait for the guard to finish. This can reduce concurrency. Use `guard *` only for small, tightly scoped operations. --- #### The `recover` Block The `recover` block is optional. If omitted, the guard silently restores protected state and execution continues after `endguard`. If present, it runs only if the guard finishes poisoned: * Guarded `data`, `text`, and `sequence` declarations have already been reverted * Guarded sequential paths have already been repaired * Guarded variables have already been restored * `recover err` binds the final poison error; read `err.message` for a combined message or inspect `err.errors` in host JavaScript. The variable name is optional; bare `recover` (without a binding) is also valid > Note: If all errors are detected and repaired inside the guard (using `is error`), the guard is considered successful and no recovery occurs. #### Manually Reverting Guarded State > **Work in progress:** The `revert` statement for manually resetting guarded state inside a `guard` block is not yet available in script mode. When implemented, `revert` will reset all `data`, `text`, and `sequence` declarations in the current scope to their state at the start of the nearest enclosing scope boundary (e.g., the start of the `guard` block). This provides fine-grained control complementing automatic guard recovery. #### Error Handling with Sequential Operations ```javascript db!.beginTransaction() var insertResult = db!.insert("users", userData) var updateResult = db!.update("profiles", profileData) var status var errorMsg if db! is error db!!.rollback() status = "transaction_failed" errorMsg = db!#message else db!.commit() status = "success" endif return { status: status, error: errorMsg } ``` ## Return Statements Use `return` to explicitly shape what a script, function, method, or call block produces. After a `return` runs, later statements in that same callable body are skipped. ```javascript // Return a simple value return 42 // Return no value return // Return an explicit null value return none // Return a variable return user // Return an object literal return { name: user.name, count: items.length } // Build with plain variables and return them directly var report = { name: user.name, count: items.length } return report // Use snapshot() only when you are intentionally observing ordered work data reportData reportData.user.name = "Alice" return reportData.snapshot() ``` `snapshot()` captures the assembled value at that point, waiting for all pending writes to complete. It can be called anywhere after the declaration is made. For most cases, returning a `var` or a plain object literal is simpler than declaring `data`, `text`, or `sequence`. Use `data`/`text` when you need ordered writes, structured path updates, or text building; use `sequence` when you need an ordered interface to an external object. If no `return` runs, or if you use bare `return`, the JavaScript API resolves with `null`, the same value used for Cascada `none`. ## Composition and Loading When a project grows beyond a single file, CascadaScript provides two file-composition tools plus component instances: - **`import`** - load a library of reusable functions from another file - **`extends` / `method`** - inherit a base script's structure and override specific behaviors - **`component`** - create isolated, independently-stateful instances of a script hierarchy `import` and `component` use `with` to pass input values into the composed file. `extends` is different: the render context flows through the inheritance chain automatically. These are covered in detail in the sections below. ### Importing Libraries with `import` Use `import` to share public root-scope declarations across multiple scripts - helper functions, reusable constants, and assembled channel values - without duplicating them. Public declarations are root-scope names that do not start with `_`. Exported non-`shared` channel declarations are exposed through their final snapshots, so importing a `text` or `data` channel gives the assembled value rather than the channel object itself. `shared` declarations belong to `extends`/`component` state and are accessed through `this.`, not through import namespaces. #### Importing a Namespace with `as` Bind the library to a name and call its functions through that namespace: ```cascada // formatters.script function formatUser(user) return user.firstName + " " + user.lastName endfunction ``` ```cascada // main.script import "formatters.script" as fmt var user = fetchUser(1) return { name: fmt.formatUser(user) } ``` This returns `{ name: "Alice Durand" }`. #### Importing Specific Names with `from` Pull specific functions directly into the caller's namespace instead: ```cascada // formatters.script - same file as above function formatUser(user) return user.firstName + " " + user.lastName endfunction ``` ```cascada // main.script from "formatters.script" import formatUser var user = fetchUser(1) return { name: formatUser(user) } ``` This returns `{ name: "Alice Durand" }`. Use `as` when importing several functions from the same library; use `from ... import` when you only need one or two specific names directly in scope. #### Passing Values to Libraries with `with` A library can read payload values passed by the caller with `with`. Here the same library is enriched with a configurable `locale`: ```cascada // formatters.script function formatUser(user) var selectedLocale = locale or "en" return user.firstName + " " + user.lastName + " [" + selectedLocale + "]" endfunction ``` ```cascada // main.script var locale = "fr" import "formatters.script" as fmt with locale var user = fetchUser(1) return { user: fmt.formatUser(user) } ``` This returns `{ user: "Alice Durand [fr]" }`. Instead of passing an explicit var, you can expose the render context as payload: ```cascada // main.script - locale comes from the render context, no child var needed import "formatters.script" as fmt with context var user = fetchUser(1) return { user: fmt.formatUser(user) } ``` This returns `{ user: "Alice Durand [en-GB]" }` when `locale` comes from the render context. `from ... import` follows the same `with` rules. Named inputs always take priority over context lookup. The full payload rules are in the next section. ### `with`: Composition Payload `with` applies to `import` and `component`. It does not apply to `extends` - the render context flows through an `extends` chain automatically. **`with varName, ...`** - passes the named local `var`s by value. Only `var` declarations can be listed; `data`, `text`, and `sequence` declarations cannot cross a composition boundary. **`with { key: expr, ... }`** - an explicit object literal; keys become named inputs inside the child, values are expressions evaluated in the caller's scope. **`with context`** - makes the render context available to bare-name lookups inside the child. Does not expose parent local variables or channel declarations. Forms can be combined. The `{ key: value }` object form must come last: ```cascada import "formatters.script" as fmt with context, locale import "formatters.script" as fmt with context, { locale: "fr" } component "widget.script" as w with context, theme, { size: "lg" } ``` **Resolution order**: explicit `with` value, then `with context` lookup, then globals. ### Script Inheritance with `extends`, `shared`, and `method` Use inheritance when multiple scripts do mostly the same thing but differ in specific steps. A base script defines the shared logic and declares override points; each child replaces only the parts that differ. Three concepts work together: - **Methods** - named override points declared in the base, called with `this.method(...)`. - **Constructors** - the script body; call `super()` to run the parent constructor. - **Shared variables** - declared with `shared var`, `shared data`, etc.; accessible via `this.name` from any method or constructor in the chain. #### Methods A `method` is a named, overridable function. Declare it with `method ... endmethod` and call it with `this.methodName(...)`. The `this.` prefix makes it an inherited method call - without it, the call is an ordinary local or context function call. ```cascada // page.script method title() return "Home" endmethod return { title: this.title() } ``` ```javascript await env.renderScript("page.script", {}) // { title: "Home" } ``` A method can take arguments and return a value, just like a function. Writing `this.method` without `(...)` is an error - inherited methods must be called, not read as values. Declaring both a shared variable and a method with the same name in the same file is an error. #### Overrides A child script can override a method from the base by declaring one with the same name. When the base calls `this.title()`, the child's version runs - even though the call site is in the base. ```cascada // about.script extends "page.script" method title() return "About Us" endmethod ``` ```javascript await env.renderScript("about.script", {}) // { title: "About Us" } ``` The base script is not duplicated. Any logic in the base constructor - fetching data, building the result - runs unchanged with the child's overrides in place. An override may have fewer trailing arguments than the parent, but any kept argument must use the same name. Callers can pass arguments by keyword (e.g. `this.card(user=profile)`), so renaming a kept position would change the public call contract: ```cascada // base.script method card(user, theme) return user.name + " / " + theme endmethod ``` ```cascada // child.script extends "base.script" method card(user) return super(user, "dark") endmethod ``` Declaring `card(profile)` instead of `card(user)` would be an error - `profile` is at the same position as the parent's `user`, which callers refer to by name. #### `super()` and `super(...)` Use `super()` to call the parent's version of the method and build on its result rather than replacing it entirely. `super()` passes no arguments: ```cascada // about.script extends "page.script" method title() return super() + " - About Us" endmethod ``` ```javascript await env.renderScript("about.script", {}) // { title: "Home - About Us" } ``` Pass arguments explicitly when the parent needs them: ```cascada method greet(name, formal) return super("Anonymous", formal) endmethod ``` #### Constructors The script body after `extends` is the constructor for that level of the chain. If the child does work of its own, that work runs first. Call `super()` at the point where the parent constructor should run. ```cascada // about.script extends "page.script" method title() return "About Us" endmethod // Child setup - runs first. var extra = loadSidebar() super() // parent constructor runs here ``` If a child only overrides methods and does no setup of its own, it uses the nearest parent constructor. Once a child does have setup code, parent constructors run only when the child calls `super()`. `super()` returns the parent constructor's return value. Use `return super()` to forward it as the entry result. #### Shared Variables Shared variables belong to the running chain and are accessible via `this.name` from any constructor or method, regardless of which file the code is in. Declare them with `shared var`, `shared data`, `shared text`, or `shared sequence`. ```cascada // page.script shared var theme = "light" method title() return "Home" endmethod return { title: this.title(), theme: this.theme } ``` ```cascada // dark-page.script shared var theme = "dark" // first default in child-to-parent order wins extends "page.script" ``` ```javascript await env.renderScript("dark-page.script", {}) // { title: "Home", theme: "dark" } ``` **Every file that uses `this.name` must declare it** - files compile independently, so the compiler needs the declaration to know what `this.name` refers to. Re-declaring with a different channel type is an error. Shared variables can only be written through `this.name`. Writing `theme = "dark"` does not reach the shared variable - it assigns a local instead. `shared` declarations must appear before `extends` and at the top level of the script - they are not allowed inside method or constructor bodies. The full set of declaration forms: | Declaration | Accessed as | |---|---| | `shared var x = default` | `this.x` (read/snapshot), `this.x = value` (write), `this.x.prop` (read-through), `this.x.prop = value` (nested write) | | `shared var x` | same access forms; no default - a parent default may still initialize `x` | | `shared data x` | `this.x.push(...)`, `this.x.path = value`, etc. | | `shared text x` | `this.x("message")` | | `shared sequence db = expr` | `this.db.method(...)` | Any shared variable can also be checked for errors: `this.x is error`, `this.x#`. Plain names do not read shared variables. `this.theme` reads the shared variable; plain `theme` looks up the render context, locals, and globals. Variables created in the constructor body are local to that constructor. Use shared variables for values that other methods or parent constructors also need. --- #### Context and initialization in `extends` The render context carries through an `extends` chain automatically - every constructor and method at every level sees it by bare name. `extends` takes no `with` clause. To pass initialization values between levels, use shared defaults. The first declaration with an initializer in child-to-parent order initializes the shared variable. A declaration without an initializer does not block parent defaults; use `= none` when the child must explicitly choose an empty default. ```cascada // dark-page.script shared var theme = "dark" // child default wins extends "page.script" ``` ```cascada // page.script shared var theme = "light" // only evaluated if no earlier default exists method title() return "[" + this.theme + "] Home" endmethod return { title: this.title() } ``` Defaults are selected before constructors run. Later constructor assignments are ordinary writes: they happen wherever the constructor body puts them. A child default overrides a parent default, but a child assignment before `super()` can still be overwritten by the parent constructor. #### Direct Render vs. Component An inheritance chain runs in one of two modes: **Direct render** - render a child script the normal way and get a result back. The chain runs once, the constructors execute, and the result is returned just like any other script. **Component** - create a named, isolated instance that lives in the current scope. It has its own shared variables and its own constructor run, but does not return a value. You call methods on it and observe its shared variables for as long as you need it. Multiple components of the same script or template coexist without interfering with each other. Unlike `extends`, a component sees only what you explicitly pass - it receives no context by default. Use `with` to pass input values; add `with context` to also pass the caller's context. Supported forms: ```cascada component "X" as ns with { initialTheme: "dark" } component "X" as ns with theme, id component "X" as ns with context component "X" as ns with context, { initialTheme: "dark" } component "X" as ns with context, theme, id ``` `with context` and explicit values can be combined. When a name appears in both places, the explicit value wins. The `{ key: value }` object form must come last in the clause. ```cascada // widget.script shared var theme = initialTheme or "light" method render(label) return "[" + this.theme + "] " + label endmethod ``` ```cascada // page.script component "widget.script" as header with { initialTheme: "dark" } component "widget.script" as footer with context // footer sees the caller's context var h = header.render("Header") var f = footer.render("Footer") return { header: h, footer: f } ``` `header` and `footer` are completely independent - separate shared variables, separate methods. Observe shared variables from the caller: ```cascada var snap = header.theme // shared var - implicit snapshot var len = header.log.snapshot().length // shared text/data/sequence - explicit snapshot var ok = header.log is error ``` Shared variables are read-only from the caller. Names that start with `_` are private to the component. #### Dynamic `extends` You can choose the parent at runtime based on context values: ```cascada extends config.baseScript // from render context extends tier == "pro" if isPro else "standard.script" extends baseScript if useBase else none // no parent if condition is false ``` Parent selection happens before any setup code runs, so the expression can read render-context values, globals, and — for components — the `with` payload. It cannot read variables you declare later in the script body, or `shared` values (those aren't initialized until the constructor runs). `extends` must appear before any executable code. `shared` declarations may appear before it, but they only reserve the shared channel slot - they cannot be read by the `extends` expression itself. When the target is `none`, `null`, or evaluates to either of those, the script simply has no parent: ```cascada extends none // explicitly parentless - still participates in an inheritance chain ``` This is useful when a script defines methods or shared variables but is always the root of its chain. All declared methods are still available via `this.method(...)`. Method declarations may appear after `extends` - they are compiled metadata, not setup code, so placement relative to `extends` does not affect resolution. #### Methods vs. Functions | | `function` | `method` | |---|---|---| | **Call syntax** | `name(...)` | `this.name(...)` | | **Overridable** | No | Yes - child replaces parent | | **`super()`** | Not available | Available | | **Shared variable access** | Isolated | Accessible via `this.name` | | **Use case** | Reusable utility logic | Override point for child scripts | #### Comparison to Class Inheritance | OOP concept | Cascada equivalent | Notes | |---|---|---| | `class Child extends Base` | `extends "base.script"` | File-level, not type-level | | Constructor | Script body after `extends` | No setup code - uses nearest parent constructor; no implicit `super()` | | Constructor parameters | shared variables + render context | Context flows through automatically; use `shared var` to pass computed values between levels | | Instance state (`this.x`) | `shared var x`, `shared data x`, etc. | Each file must declare the shared names it uses | | Virtual method | `method` | Called via `this.method(...)` | | `super.method(args)` | `super(args)` | Pass parent arguments explicitly | | Single render | `extends` direct render | One chain instance per render | | Multiple instances | `component "X" as ns` | Each instance is fully independent | | Multiple inheritance | Not supported | One parent per `extends` | ### Loaders and File Resolution When you write: ```cascada import "utils.script" as utils extends "base.script" ``` the environment resolves those file names through its configured loader or loaders. Loaders define: - where scripts are loaded from, such as the filesystem, a web server, a database, or a precompiled bundle - how relative paths are resolved - which source wins when multiple loaders are configured In practice: - `FileSystemLoader` loads scripts from disk - `WebLoader` loads scripts over HTTP in browser environments - `PrecompiledLoader` loads templates or scripts that were precompiled ahead of time You can pass one loader or several loaders to `AsyncEnvironment`. If multiple loaders are configured, Cascada tries them in order until one finds the requested script. The detailed loader API is documented in [API Reference](#api-reference). ## API Reference Cascada builds upon the robust Nunjucks API, extending it with a powerful new execution model for scripts. This reference focuses on the APIs specific to CascadaScript. For details on features inherited from Nunjucks, such as the full range of built-in filters and advanced loader options, please consult the official [Nunjucks API documentation](https://mozilla.github.io/nunjucks/api.html). ### Key Distinction: Script vs. Template * **Script**: A file or string designed for **logic and data orchestration**. Scripts use features like `var`, `for`, `if`, channel declarations (`data`, `text`), sequence declarations (`sequence`), and explicit `return` to execute asynchronous operations and produce a structured result. Their primary goal is to *build data*. * **Template**: A file or string designed for **presentation and text generation**. Templates use `{{ variable }}` and `{% tag %}` syntax to render a final string output. Their primary goal is to *render text*. Use ESM imports for new code. The main entry can compile from source: ```javascript import { AsyncEnvironment, FileSystemLoader, precompileScript, precompileTemplateAsync } from 'cascada-engine'; ``` Use the precompiled entry when templates or scripts are compiled ahead of time and the app only needs the runtime. This entry does not import the compiler, parser, lexer, or precompile API: ```javascript import { AsyncEnvironment, PrecompiledLoader } from 'cascada-engine/precompiled'; ``` ### AsyncEnvironment Class The `AsyncEnvironment` is the primary class for orchestrating and executing CascadaScripts. All its rendering methods return Promises. #### Execution * `asyncEnvironment.renderScript(scriptName, [context])` Loads and executes a script from a file using the configured loader. ```javascript const userData = await env.renderScript('getUser.casc', { userId: 123 }); ``` * `asyncEnvironment.renderScriptString(source, [context])` Executes a script from a raw string. ```javascript const script = ` var user = { name: "Alice" } return user `; const result = await env.renderScriptString(script); // { name: "Alice" } ``` * `asyncEnvironment.renderTemplate(templateName, [context])` * `asyncEnvironment.renderTemplateString(templateSource, [context])` Renders a traditional Nunjucks template to a string. #### Configuration * `new AsyncEnvironment([loaders], [opts])` Creates a new environment. * `loaders`: A single loader or an array of loaders to find script/template files. * `opts`: Configuration flags: * `autoescape` (default: `true`): Automatically escapes template output. * `throwOnUndefined` (default: `false`): Throw when rendering an undefined value. * `loadFailFatal` (default: `true`): How a missing or failed `import` / `from import` / `component` / `include` is handled. `true` — fatal (Nunjucks-compatible). `false` — non-fatal: `import` / `component` become [`LoadFailed`](#the-kind-property) poison (an Error Value of that kind), `include` renders empty. An array such as `['import']` makes only the listed kinds fatal. The root render and `extends` are always fatal. * `trimBlocks` (default: `false`): Remove the first newline after a block tag. * `lstripBlocks` (default: `false`): Strip leading whitespace from a block tag. * `tags`: Override template tag delimiters. ```javascript import { AsyncEnvironment, FileSystemLoader } from 'cascada-engine'; const env = new AsyncEnvironment(new FileSystemLoader('scripts'), { trimBlocks: true }); ``` **Loaders** Loaders are objects that tell the environment how to find and load your scripts and templates from a source, such as the filesystem, a database, or a network. * **Built-in Loaders:** * **`FileSystemLoader`**: (Node.js only) Loads files from the local filesystem. * **`NodeResolveLoader`**: (Node.js only) Resolves templates through Node package resolution. * **`WebLoader`**: (Browser only) Loads files over HTTP. * **`PrecompiledLoader`**: Loads assets from a precompiled JavaScript object. You can pass a single loader or an array of loaders to the `AsyncEnvironment` constructor. If an array is provided, Cascada will try each loader in order until one successfully finds the requested file. ```javascript const env = new AsyncEnvironment([ new FileSystemLoader('scripts'), new PrecompiledLoader(precompiledData) ]); ``` * **Custom Loaders:** Create a custom loader by providing a function or class. Return `null` to allow fallback to the next loader. **Loader Function:** ```javascript const networkLoader = async (name) => { const response = await fetch(`https://my-cdn.com/scripts/${name}`); if (!response.ok) return null; const src = await response.text(); return { src, path: name, noCache: false }; }; ``` **Loader Class:** | Method | Description | Required? | |---|---|:---:| | `load(name)` | Loads an asset by name. Returns string, `LoaderSource`, or `null`. | **Yes** | | `isRelative(name)` | Returns `true` if a filename is relative. | No | | `resolve(from, to)`| Resolves a relative path. | No | | `on(event, handler)` | Listens for environment events. | No | ```javascript class DatabaseLoader { constructor(db) { this.db = db; } async load(name) { const record = await this.db.scripts.findByName(name); if (!record) return null; return { src: record.sourceCode, path: name, noCache: false }; } isRelative(filename) { return filename.startsWith('./') || filename.startsWith('../'); } resolve(from, to) { const fromDir = from.substring(0, from.lastIndexOf('/')); return `${fromDir}/${to}`; } } ``` **Running Loaders Concurrently:** The `raceLoaders(loaders)` function creates a single loader that runs multiple loaders concurrently and returns the result from the first one that succeeds. ```javascript import { raceLoaders, FileSystemLoader, WebLoader } from 'cascada-engine'; const fastLoader = raceLoaders([ new WebLoader('https://my-cdn.com/scripts/'), new FileSystemLoader('scripts/backup/') ]); const env = new AsyncEnvironment(fastLoader); ``` #### Compilation and Caching * `asyncEnvironment.getScript(scriptName)` Retrieves a compiled `Script` object, loading and caching it if not already cached. * `asyncEnvironment.getTemplate(templateName)` Retrieves a compiled `AsyncTemplate` object. ```javascript const compiledScript = await env.getScript('process_data.casc'); const result1 = await compiledScript.render({ input: 'data1' }); const result2 = await compiledScript.render({ input: 'data2' }); ``` #### Adding Global Methods * `asyncEnvironment.addGlobal(name, value)` Adds a global function or object with methods accessible in all scripts and templates. ```javascript env.addGlobal('utils', { formatDate: (d) => d.toISOString(), API_VERSION: 'v3' }); // In script: var formatted = utils.formatDate(now()) ``` * `asyncEnvironment.addFilter(name, func, [isAsync])` Adds a custom filter for use with the `|` operator. * `asyncEnvironment.addFilterAsync(name, func)` Adds an async filter. * `asyncEnvironment.addDataMethods(methods)` Extends the built-in `data` channel with custom methods. ```javascript env.addDataMethods({ incrementBy: (target, amount) => (target || 0) + amount, }); // In script: name.path.incrementBy(10) ``` ### Compiled Objects: `Script` When you compile an asset, you get a reusable object that can be rendered efficiently multiple times. #### `Script` Represents a compiled CascadaScript. * `asyncScript.render([context])` Executes the compiled script with the given `context`, returning a `Promise` that resolves with the result. #### `AsyncTemplate` Represents a compiled Nunjucks Template. * `asyncTemplate.render([context])` Renders the compiled template, returning a `Promise` that resolves with the final string. ### Precompiling for Production For maximum performance, precompile your scripts and templates into JavaScript ahead of time: * `precompileScript(path, [opts])` * `precompileTemplate(path, [opts])` * `precompileTemplateAsync(path, [opts])` * `precompileScriptString(source, [opts])` * `precompileTemplateString(source, [opts])` * `precompileTemplateStringAsync(source, [opts])` The resulting JavaScript can be saved to a `.js` file and loaded using the `PrecompiledLoader`. A key option is `opts.env`, which ensures custom filters, global functions, and data methods are included in the compiled output. For compiler-free precompiled rendering, import the precompiled entry. It loads only the runtime and precompiled loader, not the compiler: ```javascript import { AsyncEnvironment, PrecompiledLoader } from 'cascada-engine/precompiled'; ``` Use `renderTemplate(...)` for precompiled templates and `renderScript(...)` for precompiled scripts. The CLI uses the same modes: ```bash cascada-precompile views --mode template cascada-precompile views --mode template-async cascada-precompile script.casc --mode script --format esm ``` **For a comprehensive guide on precompilation options, see the [Nunjucks precompiling documentation](https://mozilla.github.io/nunjucks/api.html#precompiling).** ## Development Status and Roadmap ### Differences from classic Nunjucks - **Block-local scoping:** `if`, `for`/`each`/`while`, and `switch` branches run in their own scope. `var` declarations inside them stay local unless you intentionally write to an outer variable. This avoids race conditions and keeps loops concurrent. ### Roadmap This roadmap outlines key features and enhancements that are planned or currently in progress. - **Streaming support** - see [streaming.md](streaming.md) - **Expanded Sequential Execution (`!`) Support** Enhancing the `!` marker to work on variables and not just objects from the global context. - **Function parameters by reference** Allowing functions that accept arguments by reference such as `function myFunction(var state, sequence seq, db!)`, where caller `var` and `sequence` arguments can be modified from inside the function, and sequential-path arguments (db) can be used in `!` execution paths. - **Compound Assignment for Variables (`+=`, `-=`, etc.)** Extending support for compound assignment operators to regular variables (currently only supported for data channels). - **Enhanced Error Reporting** Improving the debugging experience with detailed syntax and runtime error messages. - **Execution Replay and Debugging** A dedicated logging system to capture the entire execution trace. - **OpenTelemetry Integration for Observability** Native support for tracing using the OpenTelemetry standard. - **Robustness and Concurrency Validation** Extensive testing and validation for concurrency, poisoning, and recovery behavior.