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 while the condition is not mlog::false using jump
instruction's semantics.
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
comments and 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) 2025 Cem Geçgel [email protected]