BtScript Language Reference
BtScript is a Scheme dialect designed for reactive dataflow definitions. It compiles to C#/.NET IL for execution in Orleans grains. This reference covers the complete lexical structure, special forms, built-in functions, and language conventions.
File Extension
BtScript source files use the .bts extension. Internal compiler files (stdlib, parser) use .scm for standard Scheme compatibility.
Lexical Structure
Comments
; Single-line comment (extends to end of line)
Number Literals
Integers:
42
0
100
Floats:
3.14
100.5
0.0
String Literals
Strings are delimited by double quotes and can contain any character except unescaped quotes:
"pump-42"
"sensor.temperature"
"pump_42.discharge_pressure"
Boolean Literals
BtScript supports both Scheme-style and traditional boolean literals:
true ; Boolean true
false ; Boolean false
#t ; Scheme-style true
#f ; Scheme-style false
Symbols
Symbols serve as identifiers and can include letters, digits, and certain punctuation:
temp1
rolling-avg
is-valid?
set!
*required-tasks*
pump-efficiency
Symbols start with a letter or allowed punctuation, not a digit.
Pipe-Quoted Symbols
Symbols containing special characters (like semicolons) can be quoted with vertical bars. This is primarily used for BTI (Beacon Tower Interface) references:
|btft:rolling-std;1.0.0|
|acme:pump-efficiency;1.2.0|
|btff:add-values;2.1.0|
Keywords (SRFI-88)
BtScript uses SRFI-88 postfix-colon keywords for named parameters:
id:
window:
input:
as:
on-any:
value:
version:
type:
signal:
Keywords are used throughout special forms to provide named arguments and improve readability.
Duration Literals
ISO 8601 duration format is used for time-based operations:
PT5M ; 5 minutes
PT1H ; 1 hour
PT30S ; 30 seconds
PT1H30M ; 1 hour 30 minutes
Format: PT followed by optional hours (H), minutes (M), and seconds (S).
Delimiters
( ; Left parenthesis
) ; Right parenthesis
' ; Quote (for symbols/lists)
Special Forms
Special forms have evaluation rules different from function application.
Flow Definition
Defines a reactive flow with optional persistence mode:
(flow id: <symbol>
[persist: <mode>]
<form>*)
Example:
(flow id: temp-conversion
persist: async
(inputs
(celsius "sensor.temperature"))
(trigger on-any: celsius)
(let ((fahrenheit (+ (* celsius 1.8) 32)))
(emit fahrenheit-output value: fahrenheit)))
Persistence modes:
sync— Synchronous persistence after each executionasync— Asynchronous persistencetimer— Periodic persistenceon-deactivate— Persist only on grain deactivationnone— No persistence
Default is timer.
Task Definition
Defines a reusable task implementing IFlowTask. Tasks have state and are versioned:
(task id: <symbol>
version: <string>
inputs: ((<name> <type>) ...)
output: <type>
[require: (<bti-ref> ...)]
<body>)
Example:
(task id: compute-stats
version: "1.0.0"
inputs: ((values double[]) (scale double))
output: double
require: (|btft:rolling-avg;1.0.0|)
(let* ((total (array-sum values))
(count (array-length values))
(avg (/ total count)))
(* avg scale)))
Tasks compile to C# classes with [FlowTask("btft:compute-stats;1.0.0")] attribute.
Function Definition
Defines a stateless function implementing IFlowFunction:
(function id: <symbol>
version: <string>
inputs: ((<name> <type>) ...)
output: <type>
<body>)
Example:
(function id: add-values
version: "1.0.0"
inputs: ((a double) (b double))
output: double
(+ a b))
Functions are stateless and compile to classes with [FlowFunction("btff:add-values;1.0.0")] attribute.
Input Bindings
Maps local variable names to external signal paths:
(inputs
(<symbol> <string>)*)
Simple form:
(inputs
(temp "sensor.temperature")
(pressure "pump_42.pressure")
(status "device.status"))
Typed form with signal path:
(inputs
(temp type: double signal: "sensor.temperature")
(pressure type: double signal: "pump_42.pressure")
(status type: string signal: "device.status"))
Signal paths use EndpointId format: <device>.<property>.<path>.
Task Dependencies
Declares external task dependencies with version constraints:
(require
(<bti-spec> as: <symbol>)*)
Example:
(require
(|btft:rolling-std;1.0.0| as: rolling-std)
(|acme:pump-efficiency;1.2.0| as: efficiency-calc)
(|btff:normalize;2.0.0| as: normalize))
BTI format: <prefix>:<name>;<semver> where prefix is:
btf:— Flowbtft:— Taskbtff:— Function
Trigger
Defines when the flow executes. Multiple trigger clauses can be combined:
(trigger <mode> <signal>*)
Trigger modes:
on-any: — Activate when any listed signal has a new value:
(trigger on-any: temperature pressure)
on-all: — Activate only when ALL listed signals have new values:
(trigger on-all: temp pressure flow-rate)
on-timer: / timer: — Activate on schedule:
(trigger on-timer: PT5M) ; Every 5 minutes
(trigger timer: PT1H) ; Every hour
on-change: — Activate when signal value changes:
(trigger on-change: status)
Multiple triggers:
(trigger on-any: temp pressure)
(trigger on-timer: PT5M) ; Also trigger every 5 minutes
Active vs Passive Inputs:
- Active inputs — Listed in a trigger clause; can wake the flow
- Passive inputs — NOT in any trigger clause; accessed via
(latest x)but don't trigger execution
Initialization requirement: The flow will not execute until ALL inputs (both active and passive) have received at least one value.
Gate
Internal synchronization point within a flow. Controls when subsequent code proceeds based on input availability:
(gate <mode> <signal-or-expr>*)
Gate modes:
zip: — Wait for all listed signals to have new values (synchronized sampling):
(gate zip: pressure temperature flow-rate)
; Continues only when all three signals have new values
on-timer: — Proceed on timer interval:
(gate on-timer: interval: PT1M)
Accessing cached values without synchronization:
Use (latest signal) to read the most recent cached value without waiting:
(let ((cached-temp (latest temperature)))
(emit temp-reading value: cached-temp))
Conditionals
cond
Multi-branch conditional. Evaluates tests in order, executes first matching consequent:
(cond
(<test> <consequent>)
...
(else <alternative>))
Example:
(cond
((> temp 100) (emit critical-alarm value: temp))
((> temp 80) (emit warning value: temp))
((> temp 60) (emit info value: temp))
(else (emit normal value: temp)))
when
Single-branch conditional. Executes body if test is true:
(when <test> <body>)
Example:
(when (> vibration-level 0.8)
(emit vibration-alarm value: vibration-level))
unless
Negated single-branch conditional. Executes body if test is false:
(unless <test> <body>)
Example:
(unless (= status "active")
(emit inactive-warning value: status))
Equivalent to (when (not test) body).
Local Bindings
let
Binds values to local names. All bindings are evaluated in parallel (no binding can reference another):
(let ((<symbol> <expr>)*) <body>*)
Example:
(let ((avg (/ (+ a b) 2))
(delta (- b a))
(ratio (/ b a)))
(emit statistics
value: (new Stats avg: avg delta: delta ratio: ratio)))
let*
Sequential bindings — each binding can reference previously bound names:
(let* ((<symbol> <expr>)*) <body>*)
Example:
(let* ((total (array-sum values))
(count (array-length values))
(avg (/ total count))
(variance (calculate-variance values avg)))
(emit result value: avg))
begin
Sequences multiple forms. Returns the value of the last form:
(begin <form>*)
Example:
(begin
(emit action value: "shed-max")
(emit severity value: "critical")
(emit timestamp value: (current-time)))
Define
Defines a local function (compiles to private C# method):
(define (<name> <param>*) <body>)
Example:
(define (calculate-health vib temp eff)
(/ (+ (* vib 30) (* temp 30) (* eff 40)) 100))
(let ((health (calculate-health vibration-score temp-score eff-score)))
(emit health-index value: health))
Parallel Execution
Execute branches concurrently:
(parallel
(branch as: <name> <body>*)*)
Example:
(parallel
(branch as: temp-analysis
(rolling-avg window: PT10M input: temperature as: temp-avg)
(emit temp-result value: temp-avg))
(branch as: pressure-analysis
(rolling-avg window: PT5M input: pressure as: press-avg)
(emit pressure-result value: press-avg)))
Join
Wait for named branches and combine results:
(join <branch-name>* <body>*)
Example:
(join thermal-analysis vibration-analysis
(let ((combined-health (calculate-health
thermalAnalysisResult
vibrationAnalysisResult)))
(emit health-status value: combined-health)))
Branch results are available as variables named after the branch (converted to camelCase in generated C#).
Struct Definition
Defines a named record type. Compiles to a C# class with properties:
(defstruct <name>
(<field> <type> [default: <value>] [optional:])*)
Example:
(defstruct SensorReading
(value double)
(quality int default: 100)
(timestamp long)
(unit string optional:))
Instantiation with new:
(new SensorReading
value: 42.5
quality: 95
timestamp: 1234567890)
Optional fields can be omitted. Fields with defaults use the default value if not specified.
Enum Definition
Defines an enumeration type:
(defenum <name>
<value>*
| (<value> <integer>)*)
Auto-numbered:
(defenum Status
pending
active
completed)
Explicit values:
(defenum Priority
(low 1)
(medium 5)
(high 10))
Usage:
(let ((current-status Status.active))
(when (= current-status Status.completed)
(emit finished value: true)))
Emit (Side Effects)
Publishes a value to a named output with channel routing:
(emit <target> channel: <channel> value: <expr>)
Available channels: event, alarm, notification.
Examples:
;; Channeled emit
(emit temperature-fahrenheit channel: event value: (+ (* celsius 1.8) 32))
(emit alarm-severity channel: alarm value: "critical")
(emit sensor-data channel: event value: (new Reading temp: temp press: press))
Output (Return Value)
Returns an explicit value from the flow. Flows without (output ...) return null.
(output <expr>)
Example:
(let ((f (+ (* celsius 1.8) 32)))
(output f))
A flow can use both emit (for side effects) and output (for the return value).
Generic Type Parameters
Tasks and functions can declare generic type parameters with optional constraints:
(task id: generic-transform
version: "1.0.0"
generic: (T)
constraint: ((T IComparable))
inputs: ((value T))
output: T
value)
Example:
(function id: array-first-or-default
version: "1.0.0"
generic: (T)
inputs: ((arr T[]) (default T))
output: T
(cond
((> (array-length arr) 0) (array-first arr))
(else default)))
Built-in Functions
Arithmetic Operators
| Operator | Syntax | Description |
|---|---|---|
+ | (+ a b ...) | Addition (variadic) |
- | (- a b ...) | Subtraction (variadic) |
* | (* a b ...) | Multiplication (variadic) |
/ | (/ a b ...) | Division (variadic) |
% | (% a b) | Modulo |
mod | (mod a b) | Modulo (alias) |
Examples:
(+ 10 5) ; 15
(+ 1 2 3 4) ; 10
(- 20 5) ; 15
(* 3 4 5) ; 60
(/ 100 4) ; 25
(% 17 5) ; 2
Comparison Operators
| Operator | Syntax | Description |
|---|---|---|
> | (> a b) | Greater than |
< | (< a b) | Less than |
>= | (>= a b) | Greater than or equal |
<= | (<= a b) | Less than or equal |
= | (= a b) | Equality |
Examples:
(> 10 5) ; true
(< temp 100) ; true if temp < 100
(>= pressure 50) ; true if pressure >= 50
(= status "active") ; true if status equals "active"
Logical Operators
| Operator | Syntax | Description |
|---|---|---|
and | (and a b ...) | Logical AND (short-circuit) |
or | (or a b ...) | Logical OR (short-circuit) |
not | (not a) | Logical NOT |
Examples:
(and (> temp 50) (< temp 100)) ; temp in range [50, 100)
(or (= status "error") (= status "fault"))
(not (= mode "active"))
Math Functions
| Function | Syntax | Description |
|---|---|---|
sqrt | (sqrt x) | Square root |
max | (max a b ...) | Maximum value |
min | (min a b ...) | Minimum value |
abs | (abs x) | Absolute value |
ceiling | (ceiling x) | Round up to nearest integer |
floor | (floor x) | Round down to nearest integer |
round | (round x) | Round to nearest integer |
Examples:
(sqrt 16) ; 4.0
(max 10 20 15 8) ; 20
(min temp1 temp2 temp3) ; smallest temperature
(abs -42) ; 42
(ceiling 3.2) ; 4
(floor 3.8) ; 3
(round 3.5) ; 4
Type Conversion Functions
All combinations of int, double, float, long, and string conversions follow the pattern <from>-><to> (e.g., string->int, double->float).
| Function | Description |
|---|---|
string->int | Parse string to integer |
string->double | Parse string to double |
string->float | Parse string to float |
string->long | Parse string to long |
int->string | Convert integer to string |
int->double | Convert integer to double |
int->float | Convert integer to float |
int->long | Convert integer to long |
double->int | Convert double to integer (truncate) |
double->string | Convert double to string |
double->float | Convert double to float |
float->int | Convert float to integer (truncate) |
float->double | Convert float to double |
float->string | Convert float to string |
long->int | Convert long to integer |
long->string | Convert long to string |
long->double | Convert long to double |
Examples:
(string->int "42") ; 42
(string->double "3.14") ; 3.14
(int->string 100) ; "100"
(double->int 3.9) ; 3 (truncates)
Signal Functions
| Function | Syntax | Description |
|---|---|---|
latest | (latest signal) | Get most recent cached value (returns null if never received) |
Example:
(let ((last-temp (latest temperature))
(last-pressure (latest pressure)))
(emit last-readings
value: (new Reading temp: last-temp press: last-pressure)))
Use latest to access passive inputs or to read cached values without synchronization.
Array Operations
| Function | Syntax | Description |
|---|---|---|
array | (array x y z ...) | Create array literal |
array-avg | (array-avg arr) | Average of numeric array |
array-sum | (array-sum arr) | Sum of numeric array |
array-min | (array-min arr) | Minimum value |
array-max | (array-max arr) | Maximum value |
array-length | (array-length arr) | Length of array |
array-first | (array-first arr) | First element |
array-last | (array-last arr) | Last element |
array-get | (array-get arr idx) | Get element at index |
array-take | (array-take arr n) | Take first n elements |
array-drop | (array-drop arr n) | Drop first n elements |
array-append | (array-append arr1 arr2) | Concatenate two arrays |
Examples:
(array 1 2 3 4 5) ; Create array
(array-avg temperatures) ; Average temperature
(array-sum values) ; Sum all values
(array-max pressures) ; Maximum pressure
(array-length readings) ; Number of readings
(array-take readings 10) ; First 10 readings
(array-drop readings 5) ; Skip first 5 readings
(array-get readings 0) ; First element by index
Map Operations
| Function | Syntax | Description |
|---|---|---|
map-get | (map-get m key) | Get value by key |
map-keys | (map-keys m) | Get array of all keys |
map-values | (map-values m) | Get array of all values |
map-get-or | (map-get-or m key default) | Get value by key with default |
map-contains? | (map-contains? m key) | Check if key exists |
map-count | (map-count m) | Number of key-value pairs |
map-put | (map-put m key val) | Set key-value pair (returns new map) |
map-remove | (map-remove m key) | Remove key (returns new map) |
map-merge | (map-merge m1 m2) | Merge two maps (m2 overwrites m1) |
Examples:
(map-get sensor-config "threshold")
(map-get-or config "max-temp" 100)
(map-contains? config "enabled")
(map-put config "max-temp" 100)
(map-merge defaults user-config)
Object Operations
| Form | Syntax | Description |
|---|---|---|
new | (new Type field: val ...) | Construct struct instance |
. | (. obj Property) | Member access |
-> | (-> obj .Prop .Method) | Threading macro (pipeline) |
as | (as expr Type) | Type cast |
Examples:
; Construct struct
(new SensorReading value: 42.5 quality: 100)
; Member access
(. reading value)
(. config MaxTemperature)
; Threading macro (pipeline)
(-> reading .value .ToString)
; Type cast
(as numeric-value int)
Built-in Helper Functions
rolling-avg
Computes rolling average over a time window:
(rolling-avg window: <duration> input: <signal> [as: <name>])
Example:
(rolling-avg window: PT5M input: temperature as: temp-avg)
; temp-avg contains 5-minute rolling average
Other rolling helpers include:
rolling-minrolling-maxrolling-sum
Keywords Reference
Structure Keywords
| Keyword | Context | Description |
|---|---|---|
id: | flow, task, function | Identifier for the flow/task/function |
as: | require, branch, helpers | Alias or binding name |
version: | task, function | Semantic version string (e.g., "1.0.0") |
type: | inputs, defstruct | DTDL type annotation |
persist: | flow | Persistence mode (sync, async, timer, on-deactivate, none) |
Trigger and Gate Keywords
| Keyword | Context | Description |
|---|---|---|
on-any: | trigger | Activate when any listed signal has a new value |
on-all: | trigger | Activate when ALL listed signals have new values |
zip: | gate | Wait for all signals to have new values (synchronization) |
on-timer: | trigger, gate | Timer-based activation or gate |
timer: | trigger | Timer-based activation (alias) |
on-change: | trigger | Activate when signal value changes |
on: | trigger | Subscribe to specific signal |
Parameter Keywords
| Keyword | Context | Description |
|---|---|---|
window: | rolling-avg, helpers | Time window duration (ISO 8601) |
input: | helpers | Input signal reference |
inputs: | task, function | Input parameter list |
output: | task, function | Return type |
value: | emit | Value to emit to output |
channel: | emit | Emit channel (event, alarm, notification) |
interval: | on-timer: gate | Timer interval |
signal: | inputs | External signal path |
default: | defstruct | Default field value |
optional: | defstruct | Mark field as optional |
generic: | task, function | Generic type parameters |
constraint: | task, function | Generic type constraints |
Naming Conventions
Identifiers
BtScript follows Lisp/Scheme naming conventions:
- kebab-case for multi-word names:
rolling-avg,pump-efficiency,compute-health-score - Predicates end with
?:is-valid?,null?,empty? - Mutating functions end with
!:set!,clear!,update! - Global/special variables wrapped in
*:*required-tasks*,*config*
Generated C# Names
When BtScript compiles to C#, names are automatically transformed:
| BtScript | C# Context | C# Name |
|---|---|---|
temp-avg | Variable | tempAvg |
pump-monitor | Flow class | PumpMonitorFlow |
compute-stats | Task class | ComputeStatsTask |
add-values | Function class | AddValuesFunction |
SensorReading | Struct class | SensorReading |
Rules:
kebab-case→PascalCasefor types and methodskebab-case→camelCasefor variables and parameters
Complete Examples
Flow Example: Pump Monitor
This flow demonstrates parallel analysis, task dependencies, and conditional outputs:
; Pump monitoring flow with parallel thermal and vibration analysis
(flow id: pump-monitor
; External task dependencies
(require
(|btft:rate-of-change;1.0.0| as: rate-of-change)
(|acme:pump-efficiency;1.2.0| as: efficiency-calc))
; Input signals from pump sensors
(inputs
(vibration "pump_42.accelerometer")
(temperature "pump_42.motor.temp")
(pressure "pump_42.discharge_pressure"))
; Trigger on any sensor update
(trigger on-any: vibration temperature pressure)
; Parallel analysis branches
(parallel
; Vibration analysis branch
(branch as: vib-analysis
(rolling-avg window: PT5M input: vibration as: vib-avg)
(cond
((> vib-avg 0.8)
(emit vibration-alarm value: vib-avg))))
; Thermal analysis branch
(branch as: thermal-analysis
(rolling-avg window: PT15M input: temperature as: temp-avg)
(rate-of-change input: temp-avg as: temp-rate)))
; Join thermal branch and compute efficiency
(join thermal-analysis
(let ((rated 100))
(efficiency-calc temp: thermalAnalysisResult rated: rated as: eff)
(cond
((< eff 50) (emit efficiency-critical value: eff))
((< eff 70) (emit efficiency-warning value: eff))
(else (emit efficiency-normal value: eff))))))
Task Example: Statistical Computation
; Task that computes scaled average of an array
(task id: compute-stats
version: "1.0.0"
inputs: ((values double[]) (scale double))
output: double
; Compute sum, count, and scaled average
(let* ((total (array-sum values))
(count (array-length values))
(avg (/ total count)))
(* avg scale)))
Function Example: Simple Addition
; Stateless function for adding two values
(function id: add-values
version: "1.0.0"
inputs: ((a double) (b double))
output: double
(+ a b))
Struct and Enum Example
; Define reading struct
(defstruct SensorReading
(value double)
(quality int default: 100)
(timestamp long)
(unit string optional:))
; Define status enum
(defenum DeviceStatus
offline
online
maintenance
error)
; Usage in flow
(flow id: sensor-processor
(inputs
(raw-value "sensor.value")
(raw-quality "sensor.quality"))
(trigger on-all: raw-value raw-quality)
(let ((reading (new SensorReading
value: raw-value
quality: raw-quality
timestamp: (current-timestamp)
unit: "celsius")))
(emit processed-reading value: reading)))
Advanced Example: Multi-trigger with Gates
(flow id: advanced-monitoring
(inputs
(temp type: double signal: "sensor.temp")
(pressure type: double signal: "sensor.pressure")
(flow-rate type: double signal: "sensor.flow")
(status type: string signal: "device.status"))
; Multiple triggers: sensor updates AND timer
(trigger on-any: temp pressure flow-rate)
(trigger on-timer: PT1M)
; Gate to synchronize all three sensor values
(gate zip: temp pressure flow-rate)
; Compute health score
(let* ((temp-norm (/ temp 100))
(press-norm (/ pressure 50))
(flow-norm (/ flow-rate 10))
(health (* 100 (/ (+ temp-norm press-norm flow-norm) 3))))
; Conditional output based on health
(cond
((< health 30)
(begin
(emit health-critical value: health)
(emit shutdown-required value: true)))
((< health 60)
(emit health-warning value: health))
(else
(emit health-normal value: health)))))
Reserved Words
The following symbols have special meaning and should not be used as user-defined identifiers:
Special Forms
flow task function inputs outputs require trigger gate
cond when unless else
let let* define begin
defstruct defenum
parallel branch join route
persist emit output new array as
Built-in Helpers
rolling-avg rolling-min rolling-max rolling-sum
latest time-window
Operators and Functions
+ - * / % mod
> < >= <= =
and or not
sqrt max min abs ceiling floor round
if
Boolean Literals
true false #t #f
Type System
BtScript supports DTDL (Digital Twins Definition Language) types for inputs, outputs, and struct fields:
Primitive Types
int— 32-bit integerlong— 64-bit integerfloat— Single-precision floating pointdouble— Double-precision floating pointboolean— Boolean valuestring— String value
Complex Types
<Type>[]— Array of Type (e.g.,double[],string[])Map<K, V>— Map/dictionary- Custom structs (defined via
defstruct) - Custom enums (defined via
defenum)
Type Annotations
Types are specified using the type: keyword:
(inputs
(values type: double[] signal: "sensor.readings")
(config type: Map<string,double> signal: "device.config")
(reading type: SensorReading signal: "sensor.data"))
Error Handling
BtScript flows handle errors at the runtime level. The Orleans grain infrastructure manages:
- Missing signals (flow waits until all inputs have values)
- Type mismatches (compile-time type checking)
- Task failures (grain supervision and restart)
Flows should be designed to be idempotent and handle partial data gracefully using conditionals and default values.