Basil language specification
Table of Contents
- 1. Introduction & overview
- 2. Syntax
- 3. Semantics
- 4. Appendix A: Glossary
- 5. Appendix B: Type system implementation
- 5.1. Type atoms ⚛
- 5.2. Trivial state
- 5.2.1. Type structure: (Sn,) or (Sn, Sm,)
- 5.2.2. Type list: (Sn) or (Sn Sn+1)
- 5.2.3. Sequential order: (Sn -> Sm)
- 5.2.4. Exclusive order: (Sn => Sm) implies (Sm -! Sn).
- 5.2.5. Random relationship: (Sx <> Sy)
- 5.2.6. Optional argument: (Sx? ->? Sx)
- 5.2.7. Clock argument: (Tcl Sn..) -> (Tcl -> Sn)..
- 5.2.8. Reset argument: (Trst Sn) -> Sm and (Sn Sm) <> Trst
- 5.2.9. Composer: (Sn) (Sm) -> (Sn Sm)
- 5.2.10. Extractor: (Sn Sm) -> (Sn) (Sm)
- 5.3. Type state transformers
- 5.3.1. fn=(Sn m!) :: (Sn == m!)
- 5.3.2. fn~(Tres Sn) :: ((Sn) ()) and (Sn <> Trs)
- 5.3.3. fn-(Sn Sm) :: (Sn -> Sm) and Sn <> ((Sn) ())
- 5.3.4. fn@(Tt Sn Sm) :: (Sn -> Sm) and (Sn Sm) <> Tt
- 5.3.5. fn$(Sn Sm Tcl) :: (Sn -> Sm).. and (Sn Sm) <> Tcl
- 5.3.6. fn!(Sn.. Sm.. Tcl) :: (Sn -> Sm).. and (Sn Sm) <> Tcl
- 5.4. Higher-order transformers
- 6. Appendix C: Standard library
About this document
- Throughout this document you will see terms in italics, which indicates that a definition of that term can be found in the 4. All italics terms in the glossary are singular, but may occur pluralised in the document.
- The source files of this document can be found in the Basil compiler repository. If you find a typo, formatting error, or design inconsistency, please consider creating an issue to let me know (typo PRs welcome).
- Syntax highlighting is a bit yanky because I'm not good at emacs lisp. The highlighting mode used in this document can be found in the basil-lang repo under
build/scripts/basil-mode.el
.
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.
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:
- Uniary inversion (
not
) - Multiplication, Division, Exponents,
and
,xor
- Bitshift left/right, Modulo
- Addition, Subtraction,
or
- 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 usesbreak
(which terminates the loop immediately) oryield
(which evaluates a conditional expression to decide whether to evaluate the body again). Theskip
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. Usingyield
in afor
loop is disallowed andskip
may not run the loop again if the end of the range has been reached. Thebreak
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
to2^31
) which can be written in decimal numbers (5
,-7
), hexadecimal (0xA
,-0x11
), or binary (b10011
,-b100110111011
). Number values use thenumber
keyword for type annotations. - Boolean types
- A binary truth value, either
true
orfalse
. In "hardware" these signals are implemented as a number that is always either0
or1
. Boolean values use theboolean
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 inferenceblock 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