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 .deb package 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:

PurposeOperatorExample
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:

KeywordRoleMain block
programExecutable entry pointbegin...end. — runs at program start
objectSingle-function compilation unitbegin...end. — required but empty
libraryMulti-function library modulebegin...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:

TypeWidthSignedRange (approx.)
int88-bityes−128 to 127
int1616-bityes−32,768 to 32,767
int3232-bityes−2.1 billion to 2.1 billion
int6464-bityes−9.2 × 10¹⁸ to 9.2 × 10¹⁸
uint88-bitno0 to 255
uint1616-bitno0 to 65,535
uint3232-bitno0 to 4.3 billion
uint6464-bitno0 to 1.8 × 10¹⁹
float3232-bit IEEE 754±3.4 × 10³⁸
float6464-bit IEEE 754±1.8 × 10³⁰⁸

Additional scalar types:

TypeDescription
bool8-bit boolean; values are True and False
stringUTF-32 wide string descriptor
unicodeSingle 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:

TypeSpecifierNotes
int8%hhd
int16%hd
int32%d
int64%lld
uint8%hhualso used for bool (prints 0 or 1)
uint16%hu
uint32%u
uint64%llu
float32%fscientific: %e
float64%lfscientific: %le
string%lswide string
unicode%lcwide 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:

LocationTerminatorExample
Inside a procedure or functionend;begin ... end;
Main block of a compilation unitend.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

SyntaxMeaning
pointer TA type: pointer to T
address xTake the address of variable x (analogous to &x in C)
value pDereference 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

  1. Open examples/Playground/Playground.mica in VS Code.

  2. Set a breakpoint inside ShowForLoops — click the gutter to the left of the line sum := 0; that begins the ascending loop. A red dot appears.

  3. 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.

  4. The Run and Debug panel on the left shows Locals, Watch, Call Stack, and Breakpoints.

  5. Press F10 (step over) to advance one line at a time. Watch i and sum update in the Locals panel as the loop iterates.

  6. 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 boundariesexamples/Playground exercises the complete scalar type set including pointer parameters for every type, all control flow constructs, ordinal types (enums, subranges, sets with the in membership operator), and packed aggregates.

  • Formatted inputexamples/ReadLnUsage adds Read and ReadLn with scan-count return values, unicode character input, and a const-pointer call chain.

  • Boolean evaluation semanticsexamples/ShortCircuit is a self-contained verification of short-circuit behavior across 27 test cases, organized by expression complexity.

  • Constant foldingexamples/ConstantFolding demonstrates how the compiler evaluates constant expressions at compile time under the release,constant_folding profile. Compare the assembly sizes before and after to see the effect directly.

  • Math libraryexamples/MathPower covers 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 callsexamples/MicaCallsLinux calls POSIX and Linux-specific interfaces directly from Mica source across two namespaces: posix (process identity, file operations) and linux (scheduler, memory file descriptors, kernel personality).

  • C interoperabilityexamples/MicaCallsC, examples/CCallsMica, and examples/MicaCallsMica demonstrate the full JSON-contract model for calling C libraries from Mica and Mica libraries from C. All three use the same shared Utilities library 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.