Protocols and Bindings
Protocols are the top-level executable units in Culsma.
Protocols
A protocol has a name and a statement body.
protocol FlowCytometryProtocol {
let target = tube(label = "Target", capacity = 100uL);
}Culsma also supports parameters and a tail return:
protocol Prepare(sample, volume = 10uL) {
let output = tube(label = "Output");
return output;
}The general shape is:
protocol Name(optional_params) {
statements...
return value; // optional tail return
}Protocol names are case-sensitive identifiers.
Parameters
Protocol parameters are declared in the protocol header:
protocol Prepare(sample, volume = 10uL) {
...
}Parameters may have defaults. When another protocol calls a parameterized protocol, named arguments bind to parameter names.
Module.Prepare(sample = source, volume = 5uL);Missing, unknown, duplicate, or redeclared call arguments are plan-stage diagnostics because call-frame expansion happens while building the plan.
Parameters can also drive plan-static control surfaces. For example, a protocol parameter may set a repeat count, a schedule boundary, an environment duration, or a static if condition:
protocol DiluteBatch(cycles = 3, hold_time = 10min, run_cleanup = true) {
let target = tube(label = "Target", capacity = 100uL);
let feed = tube(label = "Feed", capacity = 100uL);
repeat cycle in schedule(start = 1, end = cycles, step = 1) {
target << [feed:1uL];
}
if run_cleanup {
with env(thermal = 4C, duration = hold_time) {
hold(target);
}
}
}Local Bindings
let creates a new protocol-local binding.
let source = tube(label = "Source", capacity = 100uL);Later statements in the same protocol can refer to source. A numeric let binding captures the value calculated at that statement; later assignment to an input does not recalculate it. Quantities and units are retained through bindings and protocol arguments.
let target = tube(label = "Target", capacity = 100uL);
target << [source:5uL];let is local to the protocol body. It does not define a global material namespace and does not automatically leak into called protocols.
Assignment
Assignment updates an existing local binding:
x = 10uL;Assignment is intentionally narrow. It updates protocol-local values; it does not mutate container material state. Material movement is written with <<; author-declared composition changes use the material replacement operation.
Assignment allows:
- booleans
- integers
- text
- quantities
- restricted result-member paths such as
data_ref.result.field
It does not allow assignment to container refs, group refs, general indexed targets, or arbitrary object fields.
Returns
A protocol can return an expression:
return output;It can also use named return bindings:
return sample = output_tube, readout = observation;The tail return declares the output names. The older protocol-header returns (...) declaration remains accepted in Culsma 1.x but produces a deprecation warning. Remove the header declaration; use named tail bindings when callers need stable output names.
Returning a container returns a container reference payload. It does not collapse the result to a string name or a derived report row.
Returning a group returns a container_group_ref payload. Explicit groups and plate selector groups preserve member order:
protocol SelectedWells {
let plate96 = plate(label = "QPCR96", format = "96well", carrier_id = "PlateA");
return wells = plate96[A1:A2];
}Static indexing returns the existing ordered member; it does not allocate or copy a container:
let selected_wells = plate96[A3:A5];
let clone_a_well = selected_wells[0]; // aliases the original plate96 A3 wellThe index must be a compile-time non-negative integer and must be within the known group size.
Cross-Protocol Calls
The clearer form for calling another protocol is:
Module.ProtocolName(arg = value);The referenced protocol is expanded into concrete plan steps. Reference errors and cycles are reported by plan diagnostics.
Source Includes and Imports
Culsma source files may use top-level loading declarations:
include "path/to/file.culs";
import StdlibModule;include loads source definitions from a file. import resolves library modules. A preparation protocol can live in a separate file and return a container ready for later steps:
protocol PrepareDilution(total_volume, dilution) {
let concentrate_volume = total_volume / dilution;
let diluent_volume = total_volume - concentrate_volume;
let concentrate = tube(capacity = total_volume, load = [
content(kind = ContentKind.FORMULATION, type = ContentType.SUPPLEMENT, code = "CONCENTRATE"):concentrate_volume
]);
let solution = tube(label = "Prepared solution", capacity = total_volume, load = [
content(kind = ContentKind.FORMULATION, type = ContentType.BUFFER, code = "DILUENT"):diluent_volume
]);
solution << [concentrate];
return solution;
}include "dilution.culs";
let prepared = PrepareDilution(total_volume = 10mL, dilution = 1000);
let aliquot = tube(label = "Aliquot");
aliquot << [prepared:100uL];The preparation contains 10uL of concentrate and 9990uL of diluent before the aliquot is removed. Changing total_volume recalculates both amounts. No numeric result is hard-coded inside the preparation protocol. Dependency files contribute definitions; the entry file supplies the statements to run.
