This article is a practical introduction to writing Mica programs. It assumes familiarity with at least one compiled or structured language — the concepts are standard, but the syntax and semantics are Mica’s own. Mica is syntactically descended from Pascal, with influences from Modula-2 and Oberon, but it is a distinct language with its own type model, pointer semantics, and interoperability design.
Each section is anchored to a working example from the official tutorials repository, available at gitlab.com/mica-lang/mica-tutorials. You will build and run them as you go.
The article covers the language surface introduced in Mica 4.5 — the first public release. It does not cover C interoperability, library construction, or the full standard library. For the complete type system specification and compiler internals, see the Technical Portrait.
◈ Before You Start
Requirements
Ubuntu latest AMD64. The Mica compiler is developed and tested on this platform. Other Linux distributions may work but are not actively tested.
Mica compiler. Download the
.debpackage from mica-dev.com and install it:
sudo apt install ./mica_latest_amd64.deb
mica --version
VS Code with the Mica language extension (
mica-development-ug.mica-language) from the VS Code Marketplace. The extension provides syntax highlighting, inline error markers, and hover documentation for Mica source files.GDB for step debugging. Install it if it is not already present:
sudo apt install gdb
Getting the examples
Clone the tutorials repository and open it in VS Code:
git clone https://gitlab.com/mica-lang/mica-tutorials.git
code mica-tutorials
All examples use a consistent Makefile interface. Run these commands from the repository root:
make -C examples/<Name> # build
make -C examples/<Name> run # build and run
make -C examples/<Name> clean # remove build artifacts
Build output goes to examples/build/<Name>/. The default build produces full debug information,
so every binary you produce is immediately debuggable without a separate step.
◈ A First Program
Open examples/Cast/Cast.mica. At 66 lines it is the smallest complete example in the repository,
and it covers the mandatory skeleton of every Mica program.
program Cast;
imp
WriteLn : std;
var
i32 : int32;
i64 : int64;
f32 : float32;
f64 : float64;
procedure PrintHeader();
begin
WriteLn("Mica Official Example: Cast");
WriteLn("Part of the Mica compiler tutorial suite (Mica compiler project)");
end;
begin
PrintHeader();
i64 := 42;
WriteLn(" int32 literal -> int64: %lld", i64);
f32 := 3.14159265359 as float32;
WriteLn(" float64 literal as float32: %f", f32);
WriteLn("Done.");
end.
The skeleton
Every Mica executable begins with program <Name>;. This line declares the compilation unit as
an executable and names it — the compiler uses this name for the output binary.
The imp block imports identifiers from libraries or other compilation units. The format is
Identifier : source. Here, WriteLn is imported from std, the standard output library.
Multiple identifiers from the same source can be listed on one line, separated by commas.
The var block declares variables at module scope. Every variable carries an explicit type
annotation, separated from the name by a colon. There is no type inference; every declaration
states its type.
The main execution block starts with begin and ends with end. — note the period. This is the
program entry point. Procedure and function bodies close with end; (semicolon); only the final
end. of a compilation unit carries a period. This distinction is consistent throughout Mica.
Assignment and comparison use distinct operators that differ from most C-derived languages:
| Purpose | Operator | Example |
|---|---|---|
| Assignment | := | i64 := 42; |
| Equality | = | if a = b then |
| Inequality | # | if a # b then |
WriteLn is variadic and uses C-style format strings. It always appends a newline. Write is the
same without the newline.
Build and run
make -C examples/Cast run
Output:
Mica Official Example: Cast
Part of the Mica compiler tutorial suite (Mica compiler project)
----------------------------------------------------------------
== Implicit promotions ==
----------------------------------------------------------------
int32 literal -> int64: 42
constant folding (release build): 60
== Intended promotions (casts) ==
----------------------------------------------------------------
float64 literal as float32: 3.141593
float64 -> float32 -> float64: 3.141593
int32 -> int8 -> int32: 127
int32 -> int16 -> int64: 1000
negative int32 -> int16 -> int64: -42
Done.
Compilation unit types
Mica has three kinds of compilation unit:
| Keyword | Role | Main block |
|---|---|---|
program | Executable entry point | begin...end. — runs at program start |
object | Single-function compilation unit | begin...end. — required but empty |
library | Multi-function library module | begin...end. — required but empty |
The object and library keywords are covered in the Object Units section below.
◈ Types and Variables
Numeric types
Mica provides a complete set of fixed-width integer and floating-point types:
| Type | Width | Signed | Range (approx.) |
|---|---|---|---|
int8 | 8-bit | yes | −128 to 127 |
int16 | 16-bit | yes | −32,768 to 32,767 |
int32 | 32-bit | yes | −2.1 billion to 2.1 billion |
int64 | 64-bit | yes | −9.2 × 10¹⁸ to 9.2 × 10¹⁸ |
uint8 | 8-bit | no | 0 to 255 |
uint16 | 16-bit | no | 0 to 65,535 |
uint32 | 32-bit | no | 0 to 4.3 billion |
uint64 | 64-bit | no | 0 to 1.8 × 10¹⁹ |
float32 | 32-bit IEEE 754 | — | ±3.4 × 10³⁸ |
float64 | 64-bit IEEE 754 | — | ±1.8 × 10³⁰⁸ |
Additional scalar types:
| Type | Description |
|---|---|
bool | 8-bit boolean; values are True and False |
string | UTF-32 wide string descriptor |
unicode | Single UTF-32 code point |
Declarations
Variables are declared in a var block, one or more names followed by a colon and the type.
Multiple variables of the same type can share a declaration line:
var
i32 : int32;
i64 : int64;
f32 : float32;
f64 : float64;
a, b, c : int32; { three int32 variables }
Variables declared at the top of a compilation unit are module-scoped. Variables declared inside a procedure or function body are local to that routine and are not visible outside it.
Explicit casting
When you need a value interpreted as a narrower or different type, use the as keyword. The cast
is explicit and visible in the source:
f32 := 3.14159265359 as float32; { truncate double literal to float32 }
i32 := 127 as int8; { cast through int8 range, then widen to int32 }
i64 := 1000 as int16; { value fits; no truncation }
i64 := -42 as int16; { signed cast preserved correctly }
Integer and floating-point literals are implicitly promoted to the declared target type when the
assignment is widening. Narrowing always requires an explicit as.
Format specifiers
WriteLn and Write use C printf-style format strings. The specifier for each type:
| Type | Specifier | Notes |
|---|---|---|
int8 | %hhd | |
int16 | %hd | |
int32 | %d | |
int64 | %lld | |
uint8 | %hhu | also used for bool (prints 0 or 1) |
uint16 | %hu | |
uint32 | %u | |
uint64 | %llu | |
float32 | %f | scientific: %e |
float64 | %lf | scientific: %le |
string | %ls | wide string |
unicode | %lc | wide character |
The examples/Playground example exercises every numeric type and its format specifier in a single
run — a useful reference when writing output for unfamiliar types.
◈ Procedures and Functions
Procedures
A procedure is a named routine with no return value. Parameters are declared in a
comma-separated list, each with an explicit type annotation:
procedure PrintSection(name : string);
begin
WriteLn("== %ls ==", name);
end;
Local variables live in a var block placed between the procedure signature and its begin:
procedure ShowRepeatUntil();
var
n : int32;
total : int32;
begin
n := 1;
total := 0;
repeat
total := total + n;
n := n + 1;
until n > 10;
WriteLn(" sum (repeat until n > 10) = %d", total);
end;
Functions
A function is a routine that returns a value. The return type is declared after the parameter
list. The return value is set by assigning to the function’s own name — there is no return
keyword:
function IncrementAndReturnTrue() : bool;
begin
counter := counter + 1;
IncrementAndReturnTrue := True;
end;
Assigning to the function name sets the return value. Execution continues to the end of the body
unless leave is used to exit early.
The leave statement
leave exits a procedure or function immediately at any point in its body. In functions it is
used after assigning the return value:
function Fibonacci(n : int32) : int64;
begin
if n <= 1 then
begin
Fibonacci := n as int64;
leave; { exit; skip the recursive case }
end
else
Fibonacci := Fibonacci(n - 1) + Fibonacci(n - 2);
end;
leave takes no argument. It is the sole mechanism for early exit from a routine.
Calling convention
Mica functions comply with the System V AMD64 ABI — the same calling convention used by C on
Linux x86_64. There is no overhead at the boundary between Mica and C. This is covered in depth
in the interoperability examples (MicaCallsC, CCallsMica).
Nested procedures
Mica supports nested procedure definitions. An inner procedure declared inside an outer routine is
lexically scoped to that routine — it has access to the outer procedure’s local variables, and it
is not visible outside the enclosing scope. The examples/Nesting example demonstrates five
levels of nesting with shared variable mutation across each level.
begin…end vs. begin…end.
This distinction is worth stating clearly:
| Location | Terminator | Example |
|---|---|---|
| Inside a procedure or function | end; | begin ... end; |
| Main block of a compilation unit | end. | begin ... end. |
The period appears exactly once per source file, at the very end.
◈ Control Flow
The examples in this section are taken from examples/Playground/Playground.mica and
examples/ShortCircuit/ShortCircuit.mica.
if / then / else
if actual = expected then
begin
WriteLn(" [PASS] %ls: result=%hhu", testName, actual);
passed := passed + 1;
end
else
begin
WriteLn(" [FAIL] %ls: result=%hhu (expected %hhu)", testName, actual, expected);
failed := failed + 1;
end;
No parentheses are required around the condition. A single-statement branch does not require
begin...end; multi-statement branches do. The else branch is optional.
for / to / downto
The for loop iterates a declared ordinal variable over a range. to is ascending; downto is
descending:
{ Ascending }
sum := 0;
for i := 1 to 10 do
sum := sum + i;
WriteLn(" sum 1..10 = %d", sum); { 55 }
{ Descending }
sum := 0;
for i := 10 downto 1 do
sum := sum + i;
WriteLn(" sum 10 downto 1 = %d", sum); { 55 }
The loop variable must be declared in the enclosing var block. Enum types are valid loop
variables — the loop iterates the ordinal sequence from one enum constant to another:
for c := Red to Yellow do
begin
case c of
Red: WriteLn(" Red");
Green: WriteLn(" Green");
Blue: WriteLn(" Blue");
Yellow: WriteLn(" Yellow");
end;
end;
repeat / until
repeat...until is a post-test loop. The body always executes at least once; the loop continues
until the condition becomes True:
n := 1;
total := 0;
repeat
total := total + n;
n := n + 1;
until n > 10;
WriteLn(" sum (repeat until n > 10) = %d", total); { 55 }
n := 5;
repeat
WriteLn(" countdown: %d", n);
n := n - 1;
until n = 0;
WriteLn(" liftoff!");
The repeat and until keywords serve as delimiters — no begin...end is needed around the
body.
while / do
while...do is a pre-test loop. The body does not execute if the condition is initially False:
x := -2;
y := -1;
while x + y < 3 do
begin
x := x + 1;
y := y + 1;
WriteLn(" loop step: x =%3d y =%3d", x, y);
end;
For a single-statement body, begin...end may be omitted.
case
The case statement dispatches on an integer or ordinal value. Each branch is a constant (or
comma-separated list of constants) followed by a colon and a statement:
day := 3;
case day of
1: WriteLn(" Monday");
2: WriteLn(" Tuesday");
3: WriteLn(" Wednesday");
4: WriteLn(" Thursday");
5: WriteLn(" Friday")
else
WriteLn(" Weekend")
end;
The final branch before end or else does not require a trailing semicolon. The else branch
catches any value not matched by the listed cases; if else is omitted and no branch matches,
execution continues after end.
Enum constants work as branch labels:
color := Blue;
case color of
Red: WriteLn(" warm tone");
Green: WriteLn(" natural tone");
Blue: WriteLn(" cool tone");
Yellow: WriteLn(" bright tone");
end;
Short-circuit evaluation
and and or evaluate left to right and stop as soon as the result is determined. For
True or expr, the right side is never evaluated. For False and expr, the right side is never
evaluated. This matters when the right-side expression has side effects:
counter := 0;
result := True or IncrementAndReturnTrue();
{ counter is still 0 — right side was not called }
counter := 0;
result := False or IncrementAndReturnTrue();
{ counter is 1 — right side was called }
counter := 0;
result := False and IncrementAndReturnTrue();
{ counter is 0 — right side was not called }
The examples/ShortCircuit example verifies this behavior across 27 test cases. Build and run it:
make -C examples/ShortCircuit run
== Test summary ==
----------------------------------------------------------------
Passed: 27
Failed: 0
ALL TESTS PASSED
◈ Pointers and Mutation
Mica uses explicit pointers for any mutation through a reference. There is no implicit pass-by-reference: every parameter is passed by value unless it is declared as a pointer type.
The three operators
| Syntax | Meaning |
|---|---|
pointer T | A type: pointer to T |
address x | Take the address of variable x (analogous to &x in C) |
value p | Dereference pointer p (analogous to *p in C) |
A function that reads and then mutates through a pointer:
function AllNumericTypesAsPointers(aInt32 : pointer int32, ...) : bool;
begin
WriteLn(" *int32: %d", value aInt32); { read through pointer }
value aInt32 := value aInt32 + 1; { mutate through pointer }
end;
Calling it by passing the address of a variable:
AllNumericTypesAsPointers(address i32, ...);
WriteLn("Modified from main: int32=%d", i32);
After the call, i32 has been incremented. The mutation is visible in the caller because the
function received a pointer, not a copy. The change of ownership is explicit and visible at the
call site.
Const pointers
When a procedure needs to read through a pointer but must not mutate, declare the parameter as
const pointer T. The value dereference is read-only:
function F(const p : pointer int32) : int32;
begin
F := value p + G(p); { read and pass the const pointer further }
end;
function G(const p : pointer int32) : int32;
begin
G := value p + H(value p); { read through const pointer; pass the value }
end;
function H(p : int32) : int32;
begin
H := p + 84;
end;
This pattern from examples/ReadLnUsage shows how a const pointer passes through a call chain
without copying the pointed-to value at each call boundary.
Pointers and input
ReadLn takes addresses as arguments, in the same style as C’s scanf:
Write("Enter an integer: ");
r := ReadLn("%d", address a);
WriteLn(" int32 value: %d (result=%d)", a, r);
ReadLn returns the number of items successfully scanned. The scan count lets you detect
malformed input. Read is the non-newline-consuming variant.
Why explicit pointers
Mica has no implicit reference semantics. Any routine that modifies the caller’s data must declare
a pointer parameter, and the caller must explicitly pass address variable. Mutation is auditable
at the call site — there is no hidden aliasing to reason about.
◈ Object Units and Multi-File Builds
As a program grows, its functions are split across multiple source files. Mica uses two unit
types for this: object for single-function units, and library for multi-function modules.
The object keyword
An object unit exports one or more functions or procedures for use by other units. It has the
same structure as a program, but the begin...end. block is empty — the unit has no
independent execution path.
From examples/Playground/Fibonacci.mica:
object Fibonacci;
{ Recursive Fibonacci (shows leave) }
function Fibonacci(n : int32) : int64;
begin
if n <= 1 then
begin
Fibonacci := n as int64;
leave;
end
else
Fibonacci := Fibonacci(n - 1) + Fibonacci(n - 2);
end;
begin { Objects include an empty compound statement }
end.
The function Fibonacci is exported automatically. Any program unit compiled alongside this
object can import and call it.
Importing
The imp block in a program names what to import and where it comes from:
imp
WriteLn : std;
ProgramName, ArgCount, Arg : process;
Fibonacci : Fibonacci;
FibonacciIterative : FibonacciIterative;
Each line reads: import Identifier from unit UnitName. The unit name matches the object or
library declaration in the corresponding source file.
Recursive vs. iterative
The Playground example includes both Fibonacci implementations as separate object units. The
iterative version in FibonacciIterative.mica uses a for loop:
object FibonacciIterative;
function FibonacciIterative(n : int32) : int64;
var
a, b, temp : int64;
i : int32;
begin
a := 0;
b := 1;
if n <= 1 then
begin
FibonacciIterative := n as int64;
leave;
end
else
begin
for i := 2 to n do
begin
temp := a + b;
a := b;
b := temp;
end;
FibonacciIterative := b;
end;
end;
begin { Objects include an empty compound statement }
end.
Both produce identical results for inputs they share. The iterative version handles arbitrarily
large n without stack growth — the recursive version becomes impractical past around Fib(40):
Fib(40) = 102334155 { both versions return the same value }
Fib(90) = 2880067194370816120 { iterative only; recursive is not shown }
Multi-file compilation
Build the Playground example to compile all three source files together:
make -C examples/Playground run
The Makefile invokes the compiler with all source files listed under --source:
mica --compile --link \
--optimize debug \
--platform linux,amd64,utf-32 \
--assembly intel \
--source Playground.mica,Fibonacci.mica,FibonacciIterative.mica \
--build ./build
The compiler resolves imp declarations by matching unit names in the source file headers against
the supplied source list. There is no separate manifest or registry file.
◈ Debugging in VS Code
The tutorials repository ships a pre-configured .vscode/launch.json with three debug profiles:
- Debug Current Mica Binary — builds and debugs the example corresponding to the active editor
- Debug MicaCallsC — cross-language session: Mica program calling a C library
- Debug CCallsMica — cross-language session: C program calling a Mica library
For the basics, the first profile is the one to use.
Walkthrough
Open
examples/Playground/Playground.micain VS Code.Set a breakpoint inside
ShowForLoops— click the gutter to the left of the linesum := 0;that begins the ascending loop. A red dot appears.Press F5. VS Code presents the launch profile selector; choose Debug Current Mica Binary. The Makefile builds the example first (producing a binary with full DWARF v5 debug information), then GDB attaches to it and execution halts at your breakpoint.
The Run and Debug panel on the left shows Locals, Watch, Call Stack, and Breakpoints.
Press F10 (step over) to advance one line at a time. Watch
iandsumupdate in the Locals panel as the loop iterates.Press F11 (step into) to descend into a procedure call. Press Shift+F11 (step out) to return to the caller.
The debugger maps each machine instruction back to the Mica source line that produced it — you step through source, not assembly. This is source-level debugging with no extra configuration.
Assembly view
If you want to see the generated instructions alongside the source, use the Disassembly View button in the debug toolbar, or right-click a breakpoint and choose “Open Disassembly View”. The assembly is in Intel syntax throughout.
Debug vs. release builds
The default Makefile target builds with --optimize debug — fast compilation, full debug
information, minimal optimization. Stepping through this build behaves as expected: each source
line corresponds to a discrete machine sequence.
To build with optimizations enabled:
make -C examples/ConstantFolding MICA_OPTS=release,constant_folding
Stepping through an optimized build can be surprising. The compiler may reorder, inline, or eliminate code. Use the debug build for debugging and the release build for benchmarking or assembly inspection.
◈ What to Explore Next
The tutorials repository covers progressively more of the language surface. After working through the examples above, the natural next steps are:
All numeric type boundaries —
examples/Playgroundexercises the complete scalar type set including pointer parameters for every type, all control flow constructs, ordinal types (enums, subranges, sets with theinmembership operator), and packed aggregates.Formatted input —
examples/ReadLnUsageaddsReadandReadLnwith scan-count return values, unicode character input, and a const-pointer call chain.Boolean evaluation semantics —
examples/ShortCircuitis a self-contained verification of short-circuit behavior across 27 test cases, organized by expression complexity.Constant folding —
examples/ConstantFoldingdemonstrates how the compiler evaluates constant expressions at compile time under therelease,constant_foldingprofile. Compare the assembly sizes before and after to see the effect directly.Math library —
examples/MathPowercovers the float32 and float64 transcendental functions:Sin,Cos,Tan,Sqrt,Exp,Pow,Ln,Log10, and more, with a precision comparison between the two widths.Linux system calls —
examples/MicaCallsLinuxcalls POSIX and Linux-specific interfaces directly from Mica source across two namespaces:posix(process identity, file operations) andlinux(scheduler, memory file descriptors, kernel personality).C interoperability —
examples/MicaCallsC,examples/CCallsMica, andexamples/MicaCallsMicademonstrate the full JSON-contract model for calling C libraries from Mica and Mica libraries from C. All three use the same sharedUtilitieslibrary across four calling surfaces: scalar packets, ordinal wrappers, packed aggregates, and text.
For the complete language specification — the type system, operators, scoping rules, compilation pipeline, optimization passes, and ABI details — the Technical Portrait is the authoritative reference. The Roadmap describes what is scheduled for version 4.6 and beyond.