Basil language specification

Table of Contents

About this document

1. Introduction & overview

  • Basil is written in files ending with the .basil suffix. One or more Basil files are linked into a Basil program (or circuit), which outputs a single Factorio blueprint book string
  • As a programming language Basil can be categorised as functional and lazy
  • Basil has a structural type system which is capable of static type reflection, completeness analysis, synchronisation, and sub-typing
  • At compile-time ("during const evaluation") Basil supports meta-programming via macros, type inference, and constraints
  • At run-time ("during hardware evaluation") Basil supports dynamic assertions, error co-processing, and non-destructive type conversion (or down casting)
  • The Basil compiler outputs a Factorio blueprint string which contains both the final circuit output and its constituent parts

1.1. Goals

Building circuits inside Factorio requires a lot of clicking around multiple interfaces (while only one can be open at a time) and densely connected combinator segments quickly become unwieldy and hard to understand.

Basil aims to improve the experience developing complex circuit networks for people comfortable writing their designs as code. Importantly not only the final design but all composing parts are included in the blueprint output, making it much easier to debug generated circuits in-game. Additionally Basil comes with a range of tools for debugging (see 3.11). Hopefully Basil can give people without HDL (hardware design language) experience a gentle introduction into a lot of the same concepts, without many of the complexities of real FPGA/ hardware development.

Unfortunately Factorio is not free software, which means that users of Basil need to posses a copy of the game in order to test their designs. Currently there seems to be no open implementation of the Factorio circuit network.

1.2. Hardware platform

Factorio (from now on called "the game") implements a complex circuit network system which allows for dynamic hardware-esque programming with wires, signals, and combinator blocks. A wire can carry many different signals and the values of signal duplicates are summed together. (If component A writes X=5 and component B writes X=10 to the same wire the resulting signal will be X=15).

The game provides a finite number of signals, each representing a 32-bit signed integer (values between -2147483648 and 2147483648). A signal with value of 0 is removed from a given wire (see 2.5).

Additionally the game has three "group signals" which act on a set of input signals:

Every
Check a condition and produce an output if it passes for every input.
Any
Check a condition and produce an output if it passes for any input.
Each
Check a condition (for each input separately) and produce an output for each passing input.

The circuit network operates on a 'tick' basis. Every prime operation (i.e. those implemented via combinators in-game) takes a single tick to complete. While a circuit is evaluating it may go through several changes in intermediate values and final outputs until the final value "stabilises".

Basil explicitly doesn't guard against unstable signal values since this behaviour is required to implement multi-vibrators (i.e. memory cells). The language does however provide mechanisms for dealing with synchronisation and timing offsets.

2. Syntax

This section outlines the basic syntax elements used in Basil and gives a little bit of context on more complex topics related to a given syntax.

2.1. Comments

Comments in Basil follow the Rust convention:

  • // Regular single-line code comment
  • /// Only valid immediately followed by a statement for documentation strings
  • /* ... */ Multi-line comment block between the delimiters

2.2. Keywords

Following is a brief overview of Basil keywords and their respective usage

block, input, output
Basic syntax for circuit blocks (see 3.4)
number, boolean, structure
Type annotations (see 3.1 and 3.6)
volatile
Mark an assignment as spuriously mutable (see 3.5)
const
Mark an assignment as immutable
if, elseif, else, match, default
Flow control mechanisms (see 2.15)
loop, for, break, skip, yield
Loop control mechanisms (see 2.16)
and, or, xor, not
Boolean operations in the context of 2.15 statements or bitwise operations in the context of 2.12
assert, fuse
Run-time assertions (see 3.11)
ensure
Compile-time assertions (see 3.12)
import, export
Importing and exporting items from modules
func, inline
Inline function calls (see 3.8)
async, sync, delay
Dispatch function calls and signal synchronisation (see 3.14)
clock
Synchronised clock domains (see 3.13)
generate
(Mostly) unsynchronised clock-like (optionally repeating) counters (2.8)
layout
Physical layout configuration (tbd)
source, sink
interact with "real world" "physical" entities

2.3. Identifiers

All identifiers in Basil are utf-8 alpha-numeric text, including the special characters ? and '.

"Value identifiers" (see 2.13 and 2.9 keys) additionally allow - except as the first or last symbol (for example foo-bar or bar-1-baz').

"Path identifiers" (see 3.3 and 3.4) additionally allow _ (for example foo_bar or bar_1_baz')

This makes it possible to see at a glance whether an identifier refers to a module or concrete value, for example foo-bar is a variable, foo_bar is a module path.

Note: identifiers beginning in a - or _ respectively are reserved by the Basil compiler internals. User code may not use these identifiers, but there will be references to them in the rest of this document.

2.4. Sigils

Some identifiers in Basil carry special metadata which does not directly influence the output state of a circuit. To distinguish these type level identifiers Basil uses special sigils to indicate their fundamental incompatibility at first glance. The sigil of such an identifier is part of the identifier and its name.

@
Clock sigil (see 3.13)
!
Error sigil (see 3.11)
#
Macro sigil (see 3.7)
$
List and range sigil (see 2.6 and 2.7)
%
Higher order type sigil (See 2.8, 3.8, and more)

2.5. Null

This section is a stub

A numerical value of 0 (written as null) can not be expressed by the game hardware platform and any lexical signal reaching 0 in a block will be removed permanently from the resulting net-list.

2.6. Lists

A list is a linear collection of samely-typed values (for example only numbers or only errors). Basil uses the [ ] delimiters and space-separated items inside the list (for example [ a b c ] or [ 1 b 2 c ]).

Because of the hardware limitation of not being able to accurately express 0 all lists and ranges are 1-indexed.

2.7. Ranges

A short-hand syntax for creating a list of constant numerical values, using the .. and ..= syntax in-between two boundaries.

For example the range 1..5 creates the list [ 1 2 3 4 ], the range 1..=3 creates the list [ 1 2 3 ], etc.

Creating ranges that cross the zero-boundary (i.e. -5..=5) are not supported!

2.8. Generators

A dynamic piece of circuitry that runs through a series of values and optionally repeats. Generators are created via the generate keyword, followed by a list or range and an optional attribute set for configuration, and produce a higher-order type value.

%count-up := generate 1..100
%count-up-and-up := generate 1..100 { loops: true }

Generators can also be synchronised with a clock domain by providing a @clock.[up|down] value as the clock attribute

2.9. Attribute sets

A structured collection of named, arbitrarily-typed key-value pairs. Basil uses the { } delimiters which contains zero or more comma-separated key-value pairs in the form key: value (for example { foo: 1, bar: 5 } or { foo: x, bar: y }).

As syntactic sugar an attribute set can inherit the values of keys that are also available in its scope without having to repeat its name:

x := 10
y := { x, y: 15 }

An attribute set can be marked as "open ended" with the ... syntax (i.e. { x: 5, ... }) to allow additional attributes to be present in the set, without making any claims on what they are. This is useful when composing attribute sets, (see 3.9

This is useful in various places, for example in function inputs to allow a single attribute set to be passed into multiple (possibly recursive) functions to allow them to share data with other systems, without making incompatible claims on the attribute set contents.

Note Basil also uses the { ... } delimiters for 3.2 such as if-else statements or 3.4. Internally these concepts are actually implemented as attribute sets and any scope body can have attached attributes (see 3.2.1).

2.10. Dot-syntax

Values in structured data types (attribute sets, lists, structures) can be accessed via "dot-syntax".

For the given attribute set %attrs := { x: 5, y: { a: 10, b: 20 } the term %attrs.x equals 5, %attrs.y.a equals 10, etc.

Similarly for lists: $list := [ 1 3 7 11 ], $list.1 equals 1, $list.3 equals 7, etc.

2.11. Arbitraries

todo: pick a better name

An arbitrary piece of data that can be passed around a Basil program to configure the compilation output but is not actually included in the output circuit. An arbitrary is written between "" delimiters (it's a string) and used extensively in configuring 3.10.

2.12. Expressions

A composable operation with two or more input values producing a single output value. Not all operators can be applied to all value types (see 3.1) and operations can usually only be applied to values of the same type. Circumstances which allow for cross-type operations are mentioned on a case-by-case basis.

Following is a brief overview of available expression operations:

Syntax Description Allowed types
== Check equality numbers, booleans, lists
!= Check inequality numbers, booleans, lists
<= Less or equals numbers, lists
< Less than numbers, lists
>= Greater or equals numbers, lists
> Greater than numbers, lists
+ Addition numbers, lists
- Subtraction numbers, lists
* Multiplication numbers, lists
/ Division numbers, lists
% Modulo (remainder) numbers, lists
^ Exponent (power) numbers, lists
<< Bitshift left/ up numbers, lists
>> Bitshift right/ down numbers, lists
and Bitwise or logical AND numbers, booleans, lists
or Biwise or logical OR numbers, booleans, lists
xor Bitwise or logical XOR numbers, booleans, lists
not Bitwise or logical NOT numbers, booleans, lists

2.12.1. Evalutation precedence

Expressions are evaluated from left-to-right, following this order of precedence:

  1. Uniary inversion (not)
  2. Multiplication, Division, Exponents, and, xor
  3. Bitshift left/right, Modulo
  4. Addition, Subtraction, or
  5. Comparisons (==, !=, etc.)

2.13. Assignments

Declare a new value assignment (or "binding") from expressions using the <bind name> := <expression> syntax. A previously assigned (non-const) binding may be overwritten from inside the same scope (see 3.4). Assignment values are lazily copied when assigned to a new binding.

x := 5
const y := x
x := x + y
y := y + x // compiler error

In the above example the last line will produce a compiler error since it is trying to re-assign an exsiting const binding.

Basil also supports in-place binding manipulation through the +=, -=, *=, and /= operators. This way the above example can alternatively be written like this

x := 5
const y := x
x += y
y += x // still a compiler error

Assignments also work for attribute sets, although this usage is properly covered in 3.8.

2.14. Change detection

This section is a stub

As syntactic sugar Basil supports change detection via the -> <binding> operator, followed by either a single statement or a scope body. The change-detection becomes transparent to an assignment, i.e. the return of handle_x_changing is assigned to new_x whenever x is being changed.

block example {
  volatile x := 5
  // some sub-block changes 'x'

  // Pass x to handle_x_changing only AFTER it has changed
  new_x := x -> func handle_x_changing { x }
}

2.15. Flow control

Basil supports two ways of controlling the execution path of a circuit: if-else and match blocks.

Both if-else and match blocks accept either a conditional directly or an expression which resolves to a conditional (for example x, x < y, or x < { y + z }). Match-statements allow easy comparisons of a single value to many expected states, whereas If-statements

Interaction with higher-order types is possible via specific utility macros (see 3.7 and 6.

x := 5
y := 7

if x < 4 {
  // do 'x < 4' things
} elseif x > y {
  // do 'x > y' things
} else {
  // do everything else
}
x := 5

match x {
  1 or 3 or 5 -> { /* ... */ }
  2 or 4 or 6 -> { /* ... */ }
  default -> { /* ... */ }
}

2.16. Loops

Basil supports loops with dynamic run-condition checks via the loop keyword, followed by a child-scope. Inside the scope the keywords break, skip, and yield <value> become available.

There are two ways of constructing a loop:

Internally terminated

The loop keyword doesn't take any arguments and will loop forever unless the loop body uses break (which terminates the loop immediately) or yield (which evaluates a conditional expression to decide whether to evaluate the body again). The skip keyword will always start re-evaluating the loop body.

cond := true
loop {
  loop {
    // evaluate expression to determine whether to break or not
    yield cond and other-cond
  }

  if cond {
    // always exit loop
    break
  }
}

Range terminated

The for keyword takes a range expression (i.e. an expression which evaluates to a range) and loops at most that many times. Using yield in a for loop is disallowed and skip may not run the loop again if the end of the range has been reached. The break keyword works the same in both loop types.

Mapping the current range index is optional and uses the change detection syntax -> <bind name>.

$run-range = 1..100
for $run-range -> x {
  if x < 90 {
    skip // skip the first 89 iterations
  }
}

Loops can be synchronised with a clock domain via the clock: <clock> attribute inside the loop body. In this example the loop will run only on the rising-edge of the @my-clock clock domain. See 3.13 for details.

loop { // 'for' loops work too
  clock: @my-clock.up
  // ...
}

2.17. Modules

A Basil program can be broken up across multiple files/ modules which each do just a few things, without exposing the intermediate steps.

A module path consists of one or more module identifiers, separated by :: tokens. So for example the path core::error::#make_fuse refers to the #make_fuse macro inside the error module inside the core module.

Importing from a path is done via the import keyword, followed by a module path, ending in an attribute set containing the specific items to import. An empty attribute set imports all available items.

import const::platform { ... } // import all child paths
import core::macro { #export, #inline } // import only specific paths
import std::branch // import the branch module

By default items in a module are private and must be explicitly made visible to other modules via the export keyword, followed by a non-empty attribute set. Export paths can also be nested, to specifically re-export an item from a child path.

export {
  #create,
  ::writers { #get, #set }, ::readers::#get,
}

3. Semantics

This section explains the type and structure semantics of a Basil program.

3.1. Basic types

Type annotations aren't strictly needed but can aid code readability or improve compilation performance by skipping type inference. It also allows the Basil compiler to check the resulting circuit net-list for correctness.

Number types
32-bit signed integer values (-2^31 to 2^31) which can be written in decimal numbers (5, -7), hexadecimal (0xA, -0x11), or binary (b10011, -b100110111011). Number values use the number keyword for type annotations.
Boolean types
A binary truth value, either true or false. In "hardware" these signals are implemented as a number that is always either 0 or 1. Boolean values use the boolean keyword for type annotations.

3.2. Scope bodies

A protected section of circuit between { } delimiters. Scope bodies are implemented via 2.9 with the contents of the scope being re-written as the _scope_{scope-name}_contents attribute.

3.2.1. Scope attributes

A scope can attach attributes to configure its behaviour. Built-in scopes are often prefixed by __intern which is not allowed for user path identifiers, to avoid potential name collisions.

Attribute sets support arbitrary positional attributes (extensively used by the macro system) in addition to key-value attributes.

a := 2
%x := { a > 0, b: 7 }

// Access positional arguments via the __intern.positional list
%condition := %x.__intern.positional.0

3.3. Module system

Basil comes with a standard library of modules separated into several root modules:

core
language implementation foundation. Many keywords directly map onto one or more macros found in this module
const
exposes various constants values, mostly object and signal identifier Arbitraries which are extensively used when dealing with 3.10.
std
high-level language utilities and combinators

Module access paths are based on the filesystem paths of a given module file. For example, given the root source path /src the file /src/util/numbers.basil creates a module called numbers. A second file /src/main.basil can import from this module via the path import util::numbers { ... }.

Module paths are always traced from the location of the root module file. By convention this is either main.basil in the repository root or src/main.basil.

3.4. Blocks

Blocks provide a mechanism to structure your circuits analogous to functions in software. A block can specify inputs (arguments), outputs, and constraints.

A block is declared via the block keyword, followed by a path identifier and attribute set. Inside a block attribute set the input and output keywords become available to handle input arguments and outputs.

3.4.1. Block inputs

Input statements (using the input keyword) declare an assignment for values passed into the block as arguments. An input statement may declare multiple arguments on the same line and a block can have multiple input statements to allow splitting arguments across multiple lines.

Inputs may have type annotations for basic data types. Following is an example block that takes several types of input arguments. A block may also declare one attribute set input, which is available as _attrs inside the block.

Compile-time constraints can be applied to inputs with the ensure keyword, making sure that a block is not "called" with invalid inputs. However, it is possible for values to not yet be resolved during compile-time which will result in a compiler error if any ensure constraint exists for that value.

block example {
  // Accept any number value
  number input x

  // Accept any boolean value
  boolean input y

  // Accept an attribute set with (optional) x and y parameter
  // Access these values via _attrs.x and _attrs.y
  input { x, ensure y }

  // Accept any number greater 10
  number input z ensure > 10
}

3.4.2. Block outputs

A block can return one or more outputs of different types, with optional default return values. Attempting to return a bind that doesn't exist in that block produces a compiler error. Output types can also have type annotations added which are enforced at compile-time.

The following block returns a number, a boolean, and an attribute set containing both.

block example {
  // ...

  x := 5
  y := true
  z := { x, y }

  number output x
  boolean output y
  output z
}

3.5. Volatility

Regular assignments are only mutable in the scope they were assigned. Any child scope only has read-access to parent scope assignments.

In this example the loop block doesn't have mutable access to the value x and thus would produce a compiler error.

block outer {
  x := 1
  loop x < 10 {
    x += 1;
  }
}

Declaring the assignment of x as volatile resolves this issue.

block outer {
  volatile x := 1
  loop x < 10 {
    x += 1;
  }
}

3.6. Structure types

Basil supports higher-order structure types which are composed of other structure types and basic data types. These types are implemented as attribute sets that follow special semantic rules when following a structure keyword and path identifier.

Similar to attribute sets, values are optional by default and must be prepended with the ensure keyword to become required.

Reminder: the % sigil is required on the name and part of the identifier, i.e. this structure is called %data_set, not data_set!

structure %data_set {
  ensure number a        // any number (required)
  boolean b              // any boolean
  structure c            // any structure type
  structure %data_set d  // a nested %data_set type
}

Care must be taken when nesting structure types to avoid an infinite size type error during compilation.

3.7. Macros

This section is a stub

Macros are evaluated on the Basil intermediate representation, which has access to all type system information and lexical context of a macro's call-site (block) and associated types.

Macros are partially hygenic. A macro can not freely expand code into its context and must ultimately replace its own invocation with a new piece of Basil code.

However macros may create "helpers" (macro outputs scoped to the given call-site), which can then be used by the final macro expansion.

Macros are implemented via special compiler built-ins located in core::macro~

3.8. Functional programming

In Basil blocks follow function semantics with inputs, constraints, internal logic, and outputs. This allows blocks to be called as functions via the func keyword, which takes a module path (or function-pointer; see following sections) and attribute set (called the io-filter).

As an example, a block example which takes two parameters (x and y) can be called from another block main

block example {
  input x, y
  output z
  z := x + y
}

block main {
  sum := func example { x: 1, y: 2 }
}

A block returning a single attribute set can simply be assigned to a bind. Blocks that return a basic type and attribute set, or multiple attribute sets, need to have all their outputs bound in a new attribute set referencing all outputs.

block example {
  output x, y, z

  x := 1
  y := { a = 1 }
  z := { b = 2 }
}

block main {
  { x, y, z } := func example {}
}

Module paths can be turned into function-pointer metadata values via the inline keyword, which takes an optional attribute set which represents the base i/o-filter for the inlined block. Io-filters can be composed, meaning the "inliner" may set one parameter and depend on the caller to set the second.

3.8.1. Blocks as input arguments

Blocks can be passed as higher-order input parameters to other blocks and called as a function.

block example {
  ensure input %fn
  ensure input x, y
  output z

  z := func %fn { x, y } // assuming the passed block takes x and y parameters
}

Calling this function requires filling in the io-filter and inlining a module path as a function-pointer metadata value.

block add_xy { ... }

block main {
  func example { x: 1, y: 2, %fn: inline add_xy }
}

3.8.2. Blocks as output results

Analogously blocks can be returned from blocks via the same inline keyword.

block example {
  const x := 1
  const y := 2
  output %fn

  %fn := inline add_xy { x, y }
}

In this example the caller overrides one of the required parameters

block main {
  %fn := func example {}

  result := func %fn { x: 3 }
}

3.9. Structure type composition

This section is a stub

Basil's type system is structural, which means that types can be composed and sliced, resulting in new types. These operations are implemented via the #compose and #slice macros located in core::types

structure %a { number x, boolean y }
structure %b { number x, boolean y, number z }

#compose %a { number z } // resulting type is equal to %b
#slice %b { number z } // resulting type is equal to %a

Structure types can be anonymous (defined as part of a)

%anon-structure := #compose { number x, ... } { number y, ... }
// --> anon structure { number _0x, number _1y, ... }

3.10. Sources and Sinks

This section is still very work-in-progress

The game provides many different interfaces for circuit wires to interact with "real world" entities (such as assemblers, belts, inserts, etc).

Basil separates operations on entities into signal sources and signal sinks. For example, an assembling machine can get its contents or working state (source), and allow setting its crafting recipe or enabled state (sink), both at the same time. In Basil these would be two separate statements.

3.10.1. Signal filters

Interacting with world entities requires knowing what signals are actually produced and expected by the game. Basil uses the Factorio-internal id, so for example "Iron plate" is identified as iron-plate, "Holmium plate" as holmium-plate, etc.

Identifier values can be found in the Factorio wiki and are exported in const::entities (so for example const::entities:iron-plate, const::entities::assembling-machine-2, etc).

Since values (signals) in Basil are lexical and do not directly map to concrete in-game signals it is required to use the #filter macro found in core::entities in combination with specific item/ signal IDs to create an "entity-filter".

use core::entities { #filter }
use const::entities { iron_plate, copper_wire }
entity-filter := #filter { iron_plate < 10 && copper_wire < 20 }

The #filter macro reduces to a single boolean value which can then be checked by if or other flow control statements

3.10.2. Declaring world entities

Placable world entities (i.e. buildings) can be created via macros in the core::entities module tree (i.e. #assembler::v2), which yields a metadata value which can be interacted with through source and sink statements. Arbitraries are extensively used for entity macro configuration.

%my-assembler := #assembler:v2
%hex-lamp := #lamp { color: "#288C28" }
%rgb-lamp := #lamp { red: 40, green: 140, blue: 40 }

3.10.3. Entity sources & sinks

Declaring a source requires an entity and additional macro attributes.

use core::macro::entities;
use const::entities { iron_plate }

block main {
  %my-assembler := #assembler::v2

  volatile %contents, working-state
  source %my-assembler { %contents, working: working-state }

  enable := #filter { %contents, expected: [ iron_plate > 4 ]}

  sink my-assembler { enable }
}

Binds that hold values from a source MUST be declared as volatile, since the generated child-block

lamp
display the presence of a given signal, optionally can map different signals or values to different colours (available: red, green, blue, yellow, pink, cyan, white (default), grey, black)
numdisplay
dispay multiple digits of a given signal. The maximum number of digits must be passed as a parameter

Sinks in Basil are created with the sink keyword, which is similar to func in how it's called, except that it returns nothing.

block main {
  sink #lamp { ... }
  sink #num_display { ... }
}

3.11. Assertion and Fuses

This section is still very work-in-progress

Debugging hardware circuits is usally even more annoying than debugging software. To help with this situation Basil provides some mechanisms for reporting and collecting errors from operations.

The assert keyword takes a set of conditionals as input and either never returns or produces an error signal of the offending value in the condition. The emitted error must be assigned to/ returned from the block via an error-type binding (e.g. !not-even). Assert statements that do not assign their output to an error binding cause a compiler error. It is strongly recommended to not sync on an error value, since it may never yield a result (see 3.14).

block fails_gracefully {
  input x
  output !not-even
  !not-even := assert x % 2 == 0
}

The fuse keyword can be placed in a nested flow-control scope and fuse the outer-most lexical block of the invocation context immediately without allowing for an error to recover. However, since the block will never update again, the input values are essentially "frozen" until externally reset (todo).

block fails_permanently {
  input x

  if x % 2 != 0 {
    fuse
  }
}

3.12. Constraints

The ensure keyword creates a compile-time checked constraint on a value. There are two types of constraints:

Constrain existence

ensure is used in a prefix position to an attribute

... { ensure x, y }      // an attribute set with required x and optional y
ensure number input x, y // two required input values
Value constraint

ensure is used in a suffix position to an input statement. An input does not need type annotations to be constrained, since the desired value can be used for easy type inference

block example {
  input x ensure < 0  // accept an input which must be guaranteed to be negative
}

3.13. Clocks

This section is a stub

Basil supports creating and managing globally shared clock domains with the clock keyword. A block can bind itself to a clock domain simply by adding an input statement for a particular clock. The clock needs to have previously been created (meaning "higher up" in the hierarchy of a circuit).

Several attributes can be passed as configuration:

frequency (num ticks)
positive constant n / (UPS) specifying the length of the full clock cycle going through a single rising and falling edge.
duty-cycle (num ticks)
positive constant d < n specifying the length of the "up" section.

A clock first triggers its "rising edge", waits for duty-cycle ticks before triggering its "falling edge", waits the remainder of (frequency) - (duty-cycle) ticks, before resetting.

block main {
  @main-clock := clock {
    frequency: 60,
    duty-cycle: 50
  }
}

3.14. Asynchronous programming

This section is still very work-in-progress

Circuit design requires some careful timing considerations. If two signal paths are not equally matched but are checked together in a condition it is required to insert "meandering" in the shorter signal path to synchronise with the longer path.

This is where the sync keyword comes in, which takes two or more basic type values and waits for both to have completed "evaluating" in hardware. This behaviour is not the default since it requires the Basil compiler to insert a significant amount of additional logic around the given signals.

When the timing offset between two values is constant and known by the developer the delay keyword can be used to manually insert timing offsets for a given value.

block example {
  a := func long_function {}   // takes 10 'ticks' to complete 
  b := func short_function {}  // takes 3 'ticks' to complete

  output ab

  ab := a + delay b { t: 7 }   // Add a + b but only after both a and b have stabilised
}

The async keyword is reserved for dispatch function semantics (meaning having a block not inlined but instead shared across a clock domain. It's usage looks something like this.

block main {
  import std::ctrl::memory

  addr := 0xFF
  data := 42

  %mem-op := async memory::commit { addr, data }

  result := sync %mem-op {} // will fuse if the await fails
} 

4. Appendix A: Glossary

This section is a stub

terms in italics
definition deliberately left blank

5. Appendix B: Type system implementation

The semantics of the Basil language are implemented via virtual types in the Basil type system. Virtual types exist purely during compilation and provide composable higher-order functions, each expressing some signal state transitions and routing.

Note: this section deals with implementation specifics of the Basil language and is not required to write Basil programs. It is also a living document of the current type system model used in basil-typesys and should not be taken as finished.

Four fundamental types exist in Basil: number, boolean, attrset (attribute set), and list. All other types are built up via composition from fundamental types, i.e. a range is simply:

{
  type_id: "__intrinsic_range",
  intrinsic-range: {
    start: 1,   
    end: 19,
  }
}

5.1. Type atoms ⚛

The smallest addressible components of a Basil type notation which bootstrap the rest.

x!
constant value seeded from the input source tokens
( )
type unit able to represent nothing, a set of separate units, or a list of samely-typed units
( )..
A list of arbitrary units, may be empty
->
Linear (one-way directed) relationship (/->? marks it optional)
=>
Exclusively one-way directed relationship
<>
Randomly associated relationship (<>? marks it optionl)
-!
Forbidden relationship, assert requirements on other units
==
Assignment relationship
-?
Condition relationship
-@
Arithmetic relationship
Sx
a state x from the super-set of states S (which contain x)
Ty
a type y from the super-set of types T (which contai y)
Sn..
a list states of types (xn, xn+1)
Sx?
A state x that has been marked optional
Et
an expression t from the super-set of expressions E (which contain t). t is of form (Sn) or (Sn ? Sm)
() (and or not implies ..) ()
relationship between two type units with semantics derived from english.
/fnl
a function declaration with name l. Full syntax: fnl(input): (output)... Thus a function can be seen as a gated state relationship transformer.

5.2. Trivial state

Basic combinators to support composite signal states

5.2.1. Type structure: (Sn,) or (Sn, Sm,)

A structure modify operation which combines one or two segments, each corresponding to type statements and lists containing statements.

5.2.2. Type list: (Sn) or (Sn Sn+1)

A type structure operation which allows chaining an arbitrary number of type statements into a list.

5.2.3. Sequential order: (Sn -> Sm)

A transition from one type state to another which makes no claim about its inverse.

Given state n and m from the super-set of states S (containing both n and m), the composite expression (n -> m) and not (m -> n) is referred to as uni-linear and analogously (n -> m) and (m -> n) as bi-linear.

5.2.4. Exclusive order: (Sn => Sm) implies (Sm -! Sn).

An exclusively uni-linear state transition. Implies that no inverse connection can exist

5.2.5. Random relationship: (Sx <> Sy)

Expresses a general relationship between two types which is guaranteed to not contain linear components. Random order relationships are created and consumed in pairs and are not allowed in a root type, since this would imply an imbalance in the structure.

5.2.6. Optional argument: (Sx? ->? Sx)

Turn an optional state into an optional relationship to a concrete state.

5.2.7. Clock argument: (Tcl Sn..) -> (Tcl -> Sn)..

External clock input to a state composer which guarantees that it is only evaluated when the clock condition matches. The clock argument Tcl provides the type set ((TDown -> TUp) or (CUp -> CDown))

5.2.8. Reset argument: (Trst Sn) -> Sm and (Sn Sm) <> Trst

External reset input to a state composer which guarantees that some state is cleared. A reset argument requires a random order relationship to another composer input type.

5.2.9. Composer: (Sn) (Sm) -> (Sn Sm)

Take two type units and merge them into one

5.2.10. Extractor: (Sn Sm) -> (Sn) (Sm)

Extract a signal from a shared type unit

5.3. Type state transformers

5.3.1. fn=(Sn m!) :: (Sn == m!)

Grounding transformer: assign a signal value to a new const value

5.3.2. fn~(Tres Sn) :: ((Sn) ()) and (Sn <> Trs)

Floating transformer: Given a reset input and signal value marks the output state as floating. A random relation must exist between the signal path and reset source.

5.3.3. fn-(Sn Sm) :: (Sn -> Sm) and Sn <> ((Sn) ())

Resolver: mark an input signal as resolved to a new output signal by consuming a previously floating value.

5.3.4. fn@(Tt Sn Sm) :: (Sn -> Sm) and (Sn Sm) <> Tt

Delay an input signal by a specific amount before transitioning to a new output signal. A random relationship must exist between either the input or output signal path.

5.3.5. fn$(Sn Sm Tcl) :: (Sn -> Sm).. and (Sn Sm) <> Tcl

Generator: take an input signal and map to an output signal on clock input. Semantically a generator creates type-structure branches based on a state input set

5.3.6. fn!(Sn.. Sm.. Tcl) :: (Sn -> Sm).. and (Sn Sm) <> Tcl

Barrier: given a list of input signals wait for all values to resolve before allowing inputs to transition to their next states. Semantically a barrier is the inverse of a generator; instead of creating type-structure branches it consumes them.

5.4. Higher-order transformers

5.4.1. fn?(Sn.. Sm.. Ex) :: (Sn -> Sm).. and (! -> Sm)..

Compare a conditional expression for input states, set output state to inputs or constant values

5.4.2. fn@(Sn.. Sm.. Ex) :: (Sn -> Sm)

Apply an arithmetic expression to all input states and return the result. Input and output sets must be equal in size.

6. Appendix C: Standard library

This section is a stub