High Level Mindustry Logic (hlml) is a programming language that compiles to Mindustry Logic (mlog), which are instructions that can be run by the processors in Mindustry. It is a lot more productive than directly writing instructions.
The reference implementation is in Java, called hlml.java.
Since hlml is a high level language, it removes the need for set
, op
and
jump
instructions completely. These can be done using mutating statements,
expressions, control flow statements and procedures. Other instructions can be
invoked as procedures from the built-in scope mlog
. For example,
read result cell1 0
becomes mlog::read(result, cell1, 0)
.
Furthermore, the variables and constants that are embedded in the processor
(like @counter
) are accessed through mlog
. The variables and constants have
-
s in their name replaced with _
s, otherwise they would not be valid hlml
identifiers. For example, @phase-fabric
becomes mlog::phase_fabric
.
Similarly, instructions with a different structure depending on the first
argument are identified with a _
between the instruction name and the argument
name. For example, draw lineRect x y w h
becomes
mlog::draw_lineRect(x, y, w, h)
.
Instructions with filters (radar
, uradar
) have all the combinations
available. Filters that are not there (any
) are not written in hlml. For
example, radar enemy any any distance message1 1 unit
becomes
mlog::radar_enemy_distance(message1, 1, unit)
.
Standard library is the scope hlml
but it only has some constants for now.
See builtin.variable.hlml
and
builtin.procedure.hlml
for a list of all the symbols
in the mlog
scope and their counter-parts in Mindustry logic.
Declarations go to the top-level scope in a source file.
entrypoint
keyword can be used to declare what the processor would do when
this program is loaded. It is the same thing as the main function in a C
program.
link message1;
entrypoint { mlog::print("Hello, Mindustry!"); mlog::printflush(message1); }
Declarations that create a new symbol.
link
keyword can be used to define an identifier to represent the link between
the processor and a building.
link message1;
entrypoint { mlog::print("Hello, Mindustry!"); mlog::printflush(message1); }
as
keyword can be used to rename this link to another identifier. The compiled
instructions still use the correct link name, the hlml code only knows the
identifier given by as
.
link message1 as logger;
entrypoint { mlog::print("Hello, Mindustry!"); mlog::printflush(logger); }
using
keyword can be used to define an identifier to represent another symbol.
Aliases them selves are symbols in their own right.
using mlog::printflush as flush;
link message1;
entrypoint { mlog::print("Hello, Mindustry!"); flush(message1); }
as
keyword can be omitted, which means the definition will have the same
identifier as the aliased symbol.
using mlog::printflush;
link message1;
entrypoint { mlog::print("Hello, Mindustry!"); printflush(message1); }
const
keyword can be used to define an identifier to represent a compile-time
known value. These can have values that are numeric, color or string constants,
built-in variables and constants from mlog
scope.
link message1;
const text = "Hello, Mindustry!";
entrypoint { mlog::print(text); mlog::printflush(message1); }
const
can declare constants with expressions that have known operands. Such
expressions can be evaluated in compile time as everything necessary to evaluate
them are available. Currently, that does not cover invocations.
link cell1;
const answer = 40 + 2;
const index = 0;
entrypoint { mlog::write(answer, cell1, index); }
var
keyword can be used to define an identifier to hold a value.
link cell1;
entrypoint {
var value;
mlog::read(value, cell1, 0);
value += 10;
mlog::write(value, cell1, 0);
}
Variables can have initial values. Global variables can only have constant initial values, otherwise initialization order would create nondeterministic results.
link cell1;
var value = 0;
entrypoint {
var i = 0;
while i < 16; i++ { mlog::write(value, cell1, i); value++; }
}
proc
keyword can be used to define an identifier to hold a parametrized,
arbitrary computation.
proc set_all(cell, value) {
while var i = 0; i < 16; i++ { mlog::write(value, cell, i); }
}
link cell1;
entrypoint { set_all(cell1, 0); }
Procedures have a return value. The ones that do not explicitly return anything
return mlog::null
implicitly.
proc double(a) { return a * 2; }
entrypoint { var value = 17; value = double(value); }
When invoking a procedure, parameters from the end might not be fulfilled. In
this case, those get mlog::null
as argument.
proc double(a) { return a * 2; }
entrypoint { var value = double(); }
Procedures can have output parameters, which are marked with a trailing &
.
After the procedure ends, the values of these parameters are assigned to the
passed arguments at the invocation site. These conceptually map to the
instructions that have output parameters. For example, read result cell1 0
becomes mlog::read(result, cell1, 0)
.
proc double(a&) { a *= 2; }
entrypoint { var value = 17; double(value); }
Definitions can have the public
keyword leading them. Which means that the
definition is visible from other source files. Otherwise, all definitions are
only visible in the source file they are declared in.
Language constructs that denote instructions to be executed.
Using {}
as deliminator, statements can be combined together.
entrypoint {
{}
{
{}
}
}
Blocks form a lexical scope where the local variables do not escape.
link cell1;
entrypoint {
{
var local = 17;
mlog::write(local, cell1, 0);
}
var local = 67;
mlog::write(local, cell1, 0);
}
Using the if
keyword, control flow can branch depending on a condition. The
if
branch is executed if the condition is not mlog::false
using jump
instruction's semantics, otherwise the else
branch is executed.
link cell1;
entrypoint {
var value;
mlog::read(value, cell1, 0);
if value < 1000 { value *= 56; }
else { value *= 4; }
mlog::write(value, cell1, 1);
}
else
might be omitted. In that case, it works as if the else
had an empty
block.
link cell1;
entrypoint {
var value;
mlog::read(value, cell1, 0);
if value < 1000 { value *= 56; }
mlog::write(value, cell1, 1);
}
There could be inner variable declarations after the if
. Such variables are
only available inside the statement.
proc read(cell, index) {
var value;
mlog::read(value, cell, index);
return value;
}
link cell1;
entrypoint {
if var value = read(cell1, 0); value < 1000 {
mlog::write(value * 56, cell1, 1);
}
else {
mlog::write(value * 4, cell1, 1);
}
}
Using the while
keyword, control flow can loop depending on a condition. The
while
branch is executed if the condition is not mlog::false
using jump
instruction's semantics. Otherwise the else
branch is executed. If the while
branch is executed, the interleaved statement is executed. Then, the condition
is checked again and the while
branch is executed again if the condition is
not mlog::false
. This loops while the condition is not mlog::false
using
jump
instruction's semantics.
link cell1;
entrypoint {
var value;
var i = 1;
while i <= 1000; i++ {
value = i;
}
else {
value = 0;
}
mlog::write(value, cell1, 0);
}
else
might be omitted. In that case, it works as if the else
had an empty
block.
link cell1;
entrypoint {
var value = 0;
while value < 1000; value++ {}
mlog::write(value + 1, cell1, 0);
}
The interleaved statement might be omitted. In that case, it works as if the interleaved statement was an empty block.
link cell1;
entrypoint {
var value = 0;
while value < 1000 { value++; }
mlog::write(value + 1, cell1, 0);
}
There could be inner variable declarations after the while
. Such variables are
only available inside the statement.
link cell1;
entrypoint {
var value = 0;
while var i = 0; i < 1000; i++ { value = i; }
mlog::write(value, cell1, 0);
}
Using the break
keyword, a while
loop might be exited early.
link cell1;
entrypoint {
var i = 0;
while i < 15; i++ {
var value;
mlog::read(value, cell1, i);
if value > 0 { break; }
}
mlog::write(i, cell1, 15);
}
Loops can be labeled. Then, the break
can have a label to set which loop it
targets.
link cell1;
link cell2;
link cell3;
entrypoint {
outer: while var i = 0; i < 16; i++ {
var v1;
mlog::read(v1, cell1, i);
while var j = 0; j < 16; j++ {
var v2;
mlog::read(v2, cell2, j);
if v2 < 0 { break outer; }
mlog::write(v1 + v2, cell3, i);
}
}
}
Using the continue
keyword, a while
loop might be looped early.
link cell1;
entrypoint {
while var i = 0; i < 16; i++ {
var value;
mlog::read(value, cell1, i);
if value >= 0 { continue; }
mlog::write(-value, cell1, i);
}
}
Loops can be labeled. Then, the continue
can have a label to set which loop it
targets.
link cell1;
link cell2;
link cell3;
entrypoint {
outer: while var i = 0; i < 16; i++ {
var v1;
mlog::read(v1, cell1, i);
while var j = 0; j < 16; j++ {
var v2;
mlog::read(v2, cell2, j);
if v2 >= 0 { continue outer; }
mlog::write(v1 + v2, cell3, i);
}
}
}
Using the return
keyword, a proc
might be exited early. Then, the
procedure's return value is the value given in the return
.
proc find_first(cell, searched) {
while var i = 0; i < 16; i++ {
var value;
mlog::read(value, cell, i);
if value == searched { return i; }
}
}
link cell1;
link cell2;
entrypoint { mlog::write(find_first(cell1, 3), cell2, 0); }
The value might be omitted. In that case, it works as if the value was
mlog::null
.
proc square_root(a) {
if a < 0 { return; }
var result;
mlog::op_sqrt(result, a);
return result;
}
link cell1;
entrypoint { mlog::write(square_root(5), cell1, 0); }
Affect statements are executed solely for their effect on the program's context. These include mutate, assign and discard statements.
These are syntactic sugar for op
instruction. Using ++
and --
operators, a
variable can be incremented or decremented.
link cell1;
entrypoint {
var value;
mlog::read(value, cell1, 0);
if value < 0 { value++; }
else { value--; }
mlog::write(value, cell1, 1);
}
These are syntactic sugar for set
and op
instructions. Using =
, and
compounding it with binary operators other than comparison and logical ones
(*=
, /=
, //=
, %=
, +=
, -=
, <<=
, >>=
, &=
, ^=
, |=
), a
variable can be mutated in place.
link cell1;
entrypoint {
var value;
mlog::read(value, cell1, 0);
if value < 0 { value *= 9; }
else { value /= 9; }
mlog::write(value, cell1, 1);
}
An expression can be used as a statement. In that case, the value denoted by the expression is discarded. Such expressions are only meaningful when they have procedure invocations.
proc double(a&) { a *= 2; }
entrypoint { var value = 17; double(value); }
Language constructs that denote a value.
Expressions that denote compile-time known values.
Numbers are formed as the base, the whole part, the fraction and the exponent or
precision. The base might be omitted, in which case it is decimal. Decimals can
have an exponent with e
or E
while non decimals have precision with p
or
P
. Bases are shown with 0b
, 0o
, 0d
and 0x
which are for binary, octal,
decimal and hexadecimal numbers, respectively. Digits can have _
in between,
for readability.
link cell1;
entrypoint {
while var i = 0; i < 16; i++ {
mlog::write(0x1.ffff_ffff_ffff_fP+1023, cell1, i);
}
}
These are packed colors, which are denoted like %ff00ff
in Mindustry logic. In
HLML, these are separated from hexadecimal numbers using 0p
in the beginning.
Color constants must have 6 or 8 hexadecimal digits: every two digit is a byte
for red, green, blue and alpha channels, respectively. If the color is in 6
digits, the alpha channel is assumed to be all set. Digits can have _
in
between, for readability.
link display1;
const turquoise = 0p00_ef_ff;
entrypoint {
mlog::clear(0, 0, 0);
mlog::draw_col(turquoise);
mlog::draw_rect(20, 20, 40, 40);
mlog::drawflush(display1);
}
These are just bunch of ""
delimitated characters in the source file. (Source
file's are all handled in Unicode, but non-ASCII characters are only allowed in
comment sand strings.) There are no escape characters in HLML but Mindustry
itself understands \n
and [[
: former is used for new lines and the latter is
used for escaping color specifiers in printing (which are like
"[red]some red text"
).
link message1;
const text = "Hello, Mindustry!";
entrypoint { mlog::print(text); mlog::printflush(message1); }
This is syntactic sugar for the sensor
instruction. The "property" (which is
just a variable that is passed to sensor
) must be in the current scope! Create
an alias in this scope to access a property from another scope.
using mlog::copper as what_im_looking_for;
link container1 as container;
link cell1 as cell;
entrypoint { mlog::write(container.what_im_looking_for, cell, 0); }
Invoke a procedure by passing ()
delimitated arguments after it. Denotes the
procedure's return value.
proc double(a) { return a * 2; }
entrypoint { var value = 17; value = double(value); }
This is syntactic sugar for calling procedures, but the first argument is before the procedure name. It can only call procedures in the current scope! Create alias in this scope to call a procedure from another scope as member.
proc double(a&) { a *= 2; }
entrypoint { var value = 17; value.double(); }
These are syntactic sugar for the op
instruction.
Unary operations are promotion +
, negation -
, bitwise not~
, logical not
!
. Bitwise not has a direct representation in the op
instruction, while
other unary operations are declared as if they were in a binary expression of
the same kind where the left operand was zero (or mlog::false
).
link cell1;
entrypoint {
var a;
mlog::read(a, cell1, 0);
mlog::write(~a, cell1, 1);
}
Binary operations are multiplication *
, division /
, integer division //
,
modulus %
, addition +
, subtraction -
, left shift <<
, right shift >>
,
bitwise and &
, bitwise xor ^
, bitwise or |
, less than <
, less than or
equal to <=
, greater than >
, greater than or equal to >=
, equal to ==
,
not equal to !=
, strictly equal to ===
, logical and &&
, logical or ||
.
Other than short-circuiting logical operators, rest have direct representation
in the op
instruction. Although op
has a logical and operation, the only
difference that has from bitwise and is the return being mlog::true
or
mlog::false
instead of non-zero or zero. (Instructions that do not jump cannot
be short-circuiting by nature.) The hlml binary logical operators use jumps to
short-circuit and do not evaluate the right operand if the left operand's value
is enough the find the result. This might help to side step performance heavy
calculations or unsafe code after the left operand checks the safety.
link cell1;
entrypoint {
var a;
var b;
mlog::read(a, cell1, 0);
mlog::read(b, cell1, 1);
mlog::write(a // b, cell1, 2);
}
You can use hlml.vscode for highlighting in VS Code.
Licensed under GPL 3.0 or later.
Copyright (C) 2023 Cem Geçgel gecgelcem@outlook.com