The Nuskell programming language

Translation schemes are design algorithms that translate a chemical reaction network (CRN) into a domain-level strand displacement (DSD) system. The Nuskell programming language is inspired by the functional programming language Haskell and provides DSD specific classes, functions and macros to generalize translations for arbitrary CRNs. This section describes the syntax of the Nuskell programming language in order to add new translation schemes to the scheme library. A library of existing schemes can be found in the official Nuskell repository.

Syntax

Every translation scheme consists of variable assignments in the form of:

name = value

and functions in the form of:

declarator function(arg, ...) = value ;

The where keyword allows for more verbose formulations, the character # is used for (inline) comments:

# This is a comment.
declarator function(arg, ...) = result
  where
    result = value;                     # delimiter ';' closes the function

declarator function1(arg, ...) = result1 + result2
  where {
    result1 = value;                    # delimiter ';' separates two assignments
    result2 = function2(value)          # no delimiter ';' before closing '}'
  };

Conditional statements can be written using if, then, else, elseif keywords. The operators and, or, *, /, +, -, ==, !=, <, >, >=, <= are supported and logically equivalent to their implementation in Python:

# Operators are treated as in Python.
declarator function1(arg1, arg2, arg3, arg4) = result
  where
    result =
      if arg1 < arg2 then
        value1
      elseif arg3 and arg4 then
        value2
      else
        value3 ;

Note that white space formatting is optional, all statements above can be written on a single line. However, the use of {} and ; as delimiters is obligatory. Every function assignment has to be closed by a ;. A where statement can be followed by a single assignment, or a list of assignments {} with a ; as delimiter.

Built-in functions

The Nuskell language provides a number of built-in functions.

  • y = short() – returns a new toehold domain.

  • y = long() – returns a new branch-migration domain.

  • y = infty(x) – returns a set with one fuel from input complex x (assigns infinite concentration)

  • y = empty – return an empty set of fuels.

  • y = tail(x) – return a list x without its first element.

  • y = flip(x) – return a transposed matrix x. Similar to Python’s zip().

  • y = rev_reactions(crn) – return input crn such that corresponding irreversible reactions are combined to reversible reactions

  • y = irrev_reactions(crn) – return input crn such that reversible reactions are split into two irreversible reactions

  • y = print(m) – print message m, return nothing(!)

  • y = abort(m) – exit with message m, return nothing(!)

  • y = len(x) – returns the length of list x

  • y = sum(x) – return the sum of list x. Often used to sum over sets of fuels.

  • y = range(x) – returns a list [0 .. x-1]

  • y = reverse(x) – returns list x in reverse

  • y = map(f,x) – applies function f(x) to every element in list x

  • y = map2(f,y, x) – applies function f(y,x) to every element in list x

  • y = birxn(x)

  • y = unirxn(x)

  • y = rxn_degree(x,r)

Note that even though the functions print(m) and abort(m) do not have a return value, the Nuskell language syntax of requires them to be formulated within an assignment. For example:

# print() and abort() have no return value.
declarator function1(arg1, arg2) = result
  where {
    void = print('Computing result:');
    result =
      if arg1 < arg2 then
        abort('Error:', arg1, '<', arg2)
      else
        arg2 - arg1;
    void = print('Returning value:', result)
  };

Function declarators

Translation schemes can use a variety of function declarators to indicate function return values:

  • function – a recursive definition of a function. As an example, some of the built-in functions described above are implemented within the Nuskell language:

    function len(x) =
      if x == [] then
        0
      else
        1 + len(tail(x)) ; # tail(x) returns list x without the first element.
    
    function sum(x) = if len(x) == 0 then empty elseif len(x) == 1 then x[0] else x[0] + sum(tail(x)) ;
    function map(f, x) = if len(x) == 0 then [] else [f(x[0])] + map(f, tail(x)) ;
    
  • class – returns a domain-level complex or a list of domain-level complexes. Domain-level complexes are specified as a tuple of sequence and structure, for example: "a b a*" | "( . )" denotes a single strand with three domains forming a hairpin loop. By convention a* denotes a domain complementary to a. On the other hand, ["a b a*" | ". . .", "a b a*" | "( . )"] is a list of two molecules, which differ in their secondary structure, but not in their sequence.

    class get_complexes() = ["a b a*" | "( . )", "a b a*" | ". . ."]
      where {
        a = short(); # Note that "a*" is implicitly assigned, as the complement of "a"
        b = long()
      };
    
    # The required class "formal(s)" must read one argument (a formal species)
    # and returns a single domain-level complex.  The wildcard "?" can be used to
    # specify history domains, enabling a many to one mapping from singal to
    # formal species.
    class formal(s) = "? t f" | ". . ."
      where {
        t = short();
        f = long() };
    
    # In most cases, a class will translate a list of reactant signal species "r"
    # and product signal species "p" into a domain-level complex:
    class binary_fuel_complexes(r, p) =
      [ "a t i + b t k + ch t c + dh t d + t* dh* t* ch* t* b* t* a* t*"
      | "( ( . + ( ( . + (  ( . + (  ( . + )   )  )   )  )  )  )  )  . ",
        "a t i" | " . . . ", "t ch t dh t" | ". . . . ." ]
      where {
        a = r[0].f; # The domain f of the first signal species in list r
        b = r[1].f;
        c = p[0].f; ch = long();
        d = p[1].f; dh = long();
        i = long(); k = long();
        t = short() };
    
  • macro – has the same return value as class, but is used to denote partial domain-level complexes, i.e. a class can employ macros to generalize translation schemes on the domain-level for arbitrary CRNs.

    # TODO
    class get_fuels() = []
      where {
        [l, p, q] = flip(map(chen2D_O, reverse(prod)), 2);
        [l, p, q] = zip(map(chen2D_O, reverse(prod)));
    
  • module – returns a set() of fuel complexes. Fuel complexes are domain-level complexes with, ideally, constant concentration. Nuskell uses the keyword infty to translate a domain-level complex into a fuel complex. The example code below starts with the module main() which takes the input CRN as argument. We will discuss the properties of the crn object in detail later (see The crn object).

    # The *required* module "main(crn)" translates a CRN into a set of fuel species.
    module main(crn) = sum(map(get_fuels, crn))
      where crn = irrev_reactions(crn);
    
    module get_fuels(reaction) = sum(map(infty, complexes))
      where
        complexes = get_complexes(reaction.reactants, reaction.products) ;
    
  • global – returns a global variable, such as a global domain.

    global toehold = short() ;
    

Note: Nuskell does not enforce the proper usage of class, function, module and macro, they can be used interchangeably. Only, the global declarator is specific to the use of global variables.

The crn object

Tutorial script 1 - Fist Steps

There are two required parts: (i) the formal class defines sequence and structure of signal complexes, (ii) the main module produces a set of fuel species from the input CRN. The following translation scheme translates CRNs in binary format (two reactants, two products) into a DSD system, and aborts the translation if it encounters a non-binary reaction. The classes define signal and fuel complexes, the modules contain instructions to design fuel complexes specific for a reaction of formal species.

# -----------------------------------------------------------------------------
# Translate formal reactions with two reactants and two products.
# Lakin et. al (2012) "Abstractions for DNA circuit design." [Figure 5]
# -----------------------------------------------------------------------------
#
# Coded by Stefan Badelt (badelt@caltech.edu)

# Define a global short toehold domain
global toehold = short();

# Write a class to define domains and structure of signal species
# ? is a wildcard for a history domain.
class formal(s) = "? t f" | ". . ."
  where { t = toehold ; f = long() };

# Write a class to produce fuel complexes for bimolecular reactions
class bimol_fuels(r, p) =
  [ "a t i + b t k + ch t c + dh t d + t* dh* t* ch* t* b* t* a* t*"
  | "( ( . + ( ( . + (  ( . + (  ( . + )   )  )   )  )  )  )  )  . ",
    "a t i" | " . . . ", "t ch t dh t" | ". . . . ." ]
  where {
    a = r[0].f;
    b = r[1].f;
    c = p[0].f; ch = long();
    d = p[1].f; dh = long();
    i = long(); k = long();
    t = toehold };

# Write a module that applies the fuel production to every reaction
module rxn(r) = sum(map(infty, fuels))
  where fuels =
    if len(r.reactants) != 2 or len(r.products) != 2 then
      abort('Reaction type not implemented')
    else
      bimol_fuels(r.reactants, r.products);

# Write the module *main* that applies *rxn* to the crn.
module main(crn) = sum(map(rxn, crn))
  where crn = irrev_reactions(crn);

Tutorial script 2 - Generalization

Tutorial script 3 - Optimization

Built-In Functions

Built-in functions written in the Nuskell programming language:

function range(x) = if x == 0 then [] else range(x - 1) + [x - 1] ;

function sum(x) = if len(x) == 0 then empty elseif len(x) == 1 then x[0] else x[0] + sum(tail(x)) ;

function len(x) = if x == [] then 0 else 1 + len(tail(x)) ;

function reverse(x) = if x == [] then [] else reverse(tail(x)) + [x[0]] ;

function rxn_degree(x, r) = if len(x) == 0 then [] elseif len(x[0].reactants) == r then [x[0]] + rxn_degree(tail(x), r) else rxn_degree(tail(x), r) ;

function unirxn(x) = if len(x) == 0 then [] elseif len(x[0].reactants) == 1 then [x[0]] + unirxn(tail(x)) else unirxn(tail(x)) ;

function birxn(x) = if len(x) == 0 then [] elseif len(x[0].reactants) == 2 then [x[0]] + birxn(tail(x)) else birxn(tail(x)) ;

function map(f, x) = if len(x) == 0 then [] else [f(x[0])] + map(f, tail(x)) ;

function map2(f, y, x) = if len(x) == 0 then [] else [f(y, x[0])] + map2(f, y, tail(x))