A simple DSL for programming logic components.
If you are using Visual Studio Code, you can download an extension to make editing LogicScript files much easier.
Here are a few snippets of LogicScript code to give you a feel for the language:
// This script doesn't do anything useful
input a
input b
input'3 data
output z
output'2 out
const myconst = 123
reg'3 mem
assign out = 3
assign z = (myconst)'1
startup
@print "Hello world"
end
when *
if 1 == 2
@print "Not equal"
else
@print "Equal"
end
local $test = 1010b
@print "Test: $test hex: $test:x binary: $test:b"
$test '= $test + 1
@print $test
for $i to 5
$test = $test - $i
end
local $mul = $test * 2
out = ($mul)'2
end
Simple counter:
input add1
output'4 result
reg'4 val
when add1
val '= val + 1
result = val
end
Multiplexer:
input sel
input'2 data
output out
assign out = sel ? data[0] : data[1]
A script is executed sequentially from top to bottom. The script is composed of port declarations, const declarations and code blocks.
All values are unsigned numbers with a specified bit length, maximum 64 bits.
They can be defined in one of three formats:
- Decimal format, which is just their decimal representation
0
,23
,100
- Binary format, which is one or more zeroes or ones followed by a
b
0b
,1101b
- Hexadecimal format, which is a
0x
followed by one or more hexadecimal characters0x0
,0xF
,0xDeAdBeEf
The machine can interact with its surroundings through the use of inputs and outputs, and it can additionally store an arbitrary amount of data using registers.
Inputs can only be read from, and outputs can only be written to. They have a size of 1 bit, however they can be grouped together and be represented as a single number. Registers can be written to and read from, they can have a size from 1 to 64 bits and they persist their value.
A LogicScript script must define the inputs, outputs and registers it wants to use, along with the bit size of each of them.
They are declared simply by stating their kind, size and name, e.g.:
input'3 data
out'2 z
reg'10 mem
The size specification ('n
) can be skipped, in which case a size of 1
will be assumed for inputs and outputs, and a size of 64
will be assumed for registers.
A constant value can be declared at the top level, and it can contain a number of any size. Its value must be constant, thus it can only refer to other constants in its declaration.
const myConst = 123
There are two types of comments: line comments that start with a //
and span to the end of the line, and block comments that start with /*
and span until a */
is found.
when a == 3
// Runs if a equals 3
end
when *
// Always gets executed
end
The when
block will run its body if its condition value is truthy. Alternatively, if its condition is a single asterisk (*
), it will always be run.
startup
// Runs only once at startup
end
The startup
block will only run its body the first time the machine is updated, up to implementation.
assign z = a | b
The assign
block is a shortcut for a when *
block, its body must be an assignment and it will always be run.
The body of the blocks mentioned above consists of multiple statements, one per line (except the assign
block, which only accepts a single statement). These statements can optionally end with a semicolon (;
), which allows for multiple statements in a single line.
To read readable (input and register) ports' value, you can use the same name that was specified in the declaration.
There are three kinds of operators: unary and binary operators that have one or two operands respectively, and a ternary operator.
Binary operators have one operand on each side. These are the operators that are currently implemented, in order of precedence:
|
,&
: performs a bitwise OR or AND operation of both operands respectively.^
: performs a bitwise XOR operation of both operands.**
: raises the left number to the power of the right number.+
,-
: performs an addition or subtraction of both operands respectively.*
,/
: performs a multiplication or division of both operands respectively.
%
: returns the remainder of dividing both operands.
<<
,>>
: shifts the right operand N positions to the left or right respectively, where N is the value of the second operand.==
,>
,<
: compares both operands and returns a single bit.
!x
or~x
: returns a bitwise negation of all the bits ofx
.len(x)
: returns the bit length ofx
.allOnes(x)
: returns1
if all the bits ofx
are set to ones, and0
otherwise.rise(x)
: not implementedfall(x)
: not implementedchange(x)
: not implemented
The ternary operator has a condition operand that determines which of the other two operands will be returned. If it is truthy, the first operand will be returned, otherwise the second will be returned.
x ? a : b
The slice expression can be used to get a number consisting of a subset of another number's bits, starting from the left or the right. If the length is unspecified then 1 will be assumed.
In order to specify the starting reference, a <
or >
character can be inserted after the opening bracket to indicate "left" or "right" respectively; if neither is used, the latter will be assumed. The syntax is as follows:
[0] // Get 1 bit starting from the 1st position from the right (the offset is inclusive)
[0,3] // Get 3 bits starting from the 1st position from the right
[>4,3] // Get 3 bits starting from the 5th position from the right
[<2,2] // Get 2 bits starting from the 3rd position from the left
In order to fit a number into a slot of a smaller length, it can be truncated to a shorter bit length. If the new bit size is greater than the operand's current bit size, it will be padded with zeroes.
(123)'4 // Truncates the number into a 4 bit number
(3)'10 // Fits the number into a 10 bit number, padding with zeroes
There's also a shortcut truncation assignment, which will truncate the right side to fit into the left side.
local $var'4
$var '= 100 // Will truncate to 4 bits
Code blocks can include local variable declarations of the form local $name
or local $name = initializer
. Additionally, their bit size can be specified using a bit size declaration after its name: 'n
where n
is the number of bits. If the initializer is not present, the local will be set to 0
and its bit size must be explicitly declared.
Assignments set a writable (output or register) port or local to a given value. The value must be small enough to fit into the port or local, otherwise a parse-time error will be raised.
port = /*value*/
$var = /*value*/
Conditionally runs a list of statements if the condition expression evaluates to a truthy value. It can optionally be followed by one or more else if
statements, and finally an else
statement.
if /*condition*/
// ...
else if /*condition*/
// ...
else
// ...
end
Runs a list of statements a number of times, as defined by the "from" and "to" values. It assigns an increasing value to a local, which will be declared if it isn't already. The "from" value is inclusive, while the "to" value isn't. If the former is not specified, 0
will be assumed.
for $i from 0 to 5
// ...
end
Runs a list of statements until the condition expression is no longer truthy.
while 1
// ...
end
When used inside a for
or while
loop, it exits that loop. When used inside a when
or startup
block, it exits that block.
Task statements are used to perform actions on the machine.
The print task allows you to print a single value, or a string that can interpolate other values.
When passing a string it must be wrapped in double quotes ("
), and it can contain interpolated locals in the $name
form. Interpolated locals can be suffixed by :b
or :x
, which will format their value in binary or hexadecimal respectively.
@print $myvar
@print "The value of myvar is: $myvar, or $myvar:b in binary"
A value is considered truthy if it is not equal to zero.