Skip to main content

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 execution
  • async — Asynchronous persistence
  • timer — Periodic persistence
  • on-deactivate — Persist only on grain deactivation
  • none — 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: — Flow
  • btft: — Task
  • btff: — 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

OperatorSyntaxDescription
+(+ 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

OperatorSyntaxDescription
>(> 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

OperatorSyntaxDescription
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

FunctionSyntaxDescription
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).

FunctionDescription
string->intParse string to integer
string->doubleParse string to double
string->floatParse string to float
string->longParse string to long
int->stringConvert integer to string
int->doubleConvert integer to double
int->floatConvert integer to float
int->longConvert integer to long
double->intConvert double to integer (truncate)
double->stringConvert double to string
double->floatConvert double to float
float->intConvert float to integer (truncate)
float->doubleConvert float to double
float->stringConvert float to string
long->intConvert long to integer
long->stringConvert long to string
long->doubleConvert 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

FunctionSyntaxDescription
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

FunctionSyntaxDescription
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

FunctionSyntaxDescription
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

FormSyntaxDescription
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-min
  • rolling-max
  • rolling-sum

Keywords Reference

Structure Keywords

KeywordContextDescription
id:flow, task, functionIdentifier for the flow/task/function
as:require, branch, helpersAlias or binding name
version:task, functionSemantic version string (e.g., "1.0.0")
type:inputs, defstructDTDL type annotation
persist:flowPersistence mode (sync, async, timer, on-deactivate, none)

Trigger and Gate Keywords

KeywordContextDescription
on-any:triggerActivate when any listed signal has a new value
on-all:triggerActivate when ALL listed signals have new values
zip:gateWait for all signals to have new values (synchronization)
on-timer:trigger, gateTimer-based activation or gate
timer:triggerTimer-based activation (alias)
on-change:triggerActivate when signal value changes
on:triggerSubscribe to specific signal

Parameter Keywords

KeywordContextDescription
window:rolling-avg, helpersTime window duration (ISO 8601)
input:helpersInput signal reference
inputs:task, functionInput parameter list
output:task, functionReturn type
value:emitValue to emit to output
channel:emitEmit channel (event, alarm, notification)
interval:on-timer: gateTimer interval
signal:inputsExternal signal path
default:defstructDefault field value
optional:defstructMark field as optional
generic:task, functionGeneric type parameters
constraint:task, functionGeneric 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:

BtScriptC# ContextC# Name
temp-avgVariabletempAvg
pump-monitorFlow classPumpMonitorFlow
compute-statsTask classComputeStatsTask
add-valuesFunction classAddValuesFunction
SensorReadingStruct classSensorReading

Rules:

  • kebab-casePascalCase for types and methods
  • kebab-casecamelCase for 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 integer
  • long — 64-bit integer
  • float — Single-precision floating point
  • double — Double-precision floating point
  • boolean — Boolean value
  • string — 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.