There are programs you ship, and there are programs you write to get something done — a rename over a directory, a quick calculation, a check you will run twice and delete. Scripting languages own that second kind, and they own it for one reason: a single command runs the file.

mica --run gives Mica that command, without giving up what a compiler knows. Every command below is exactly what the released compiler does.

One command

cat > hello.mica << 'EOF'
program Hello;

imp
    WriteLn : std;

begin
    WriteLn("hello from a script");
end.
EOF

mica --run hello.mica
hello from a script

That is the whole ceremony. The compiler builds the file into a temporary directory at the debug tier, runs it, forwards its exit code, and removes every trace. Nothing appears beside your file, nothing is cached anywhere — a one-file debug compile is milliseconds, so the honest cache is no cache.

Notice what did not appear: no banner, no progress lines. On a clean compile the compiler says nothing at all, so the program owns standard output and pipes stay clean. Warnings still surface — on standard error, where diagnostics belong.

Arguments and exit codes

A script earns its keep through its arguments and its exit code. Everything after the file on the command line belongs to the program, readable through the process unit:

cat > args.mica << 'EOF'
program Args;

imp
    WriteLn : std;
    *       : process;

begin
    WriteLn("count=%d", ArgCount());
    WriteLn("first=%ls", Arg(1));
    WriteLn("second=%ls", Arg(2));
end.
EOF

mica --run args.mica alpha beta
count=2
first=alpha
second=beta

When a program’s own arguments begin with a dash, put -- between file and arguments; everything after it is passed through untouched.

The exit code works the way $? demands: the program’s verdict is the command’s verdict. Even a runtime failure reports honestly — here a program that prints one line and then indexes past an array’s bound, which trips Mica’s always-on bounds guard:

cat > trap.mica << 'EOF'
program Trap;

imp
    WriteLn : std;

type
    Numbers = array[0..2] of int64;

var
    values : Numbers;
    index  : int64;

begin
    WriteLn("before the trap");
    index := 5;
    values[index] := 1;
end.
EOF

mica --run trap.mica; echo "exit=$?"
before the trap
Mica runtime failure: reason=index_out_of_range (12), token_stream_index=40
Mica runtime context: file=.../trap.mica, line=16, column=11
Mica runtime source:     values[index] := 1;
exit=1

The environment it runs in

A script reads more than its arguments. The process unit carries the environment the shell handed over, and the program’s own identity beside it — both are things a program knows about itself that no computation can derive.

cat > editor.mica << 'EOF'
program Editor;

imp
    WriteLn, Length : std;
    Environment, IsEnvironmentSet, EnvironmentList, VariableList, Halt : process;

const
    EditorName = "EDITOR";
    Fallback   = "vi";

var
    editor : string;
    list   : VariableList;

begin
    if IsEnvironmentSet(EditorName) then
        editor := Environment(EditorName)
    else
        editor := Fallback;

    list := EnvironmentList();

    WriteLn("editor=%ls", editor);
    WriteLn("variables=%lld", Length(list));

    if IsEnvironmentSet(EditorName) then Halt(0) else Halt(3);
end.
EOF

EDITOR=nano mica --run editor.mica; echo "exit=$?"
editor=nano
variables=…
exit=0

Reading takes two verbs because there are two questions. Environment answers the text; IsEnvironmentSet answers whether the name is set at all. A variable set to nothing and a variable never set are different states, and a script deciding between them should be able to say which it meant.

VerbWhat it does
Environment(name)the text, or the empty string when nothing of that name is set
IsEnvironmentSet(name)whether the name is set at all, whatever it holds
SetEnvironment(name, text)sets or replaces it, for this program and every child it starts
RemoveEnvironment(name)takes it out, and succeeds when there was nothing to take
EnvironmentList()every variable, each divided into a name and a text
ProgramName(), ArgCount(), Arg(i)the identity the command line gave
ProcessId(), ParentProcessId()the numbers the kernel knows the program by
Halt(code)ends the program with that status, everything printed flushed first

The two writers meet the operating system, so they fail on the language’s own channel: a name that is empty or itself holds = arrives as Einval, the shapes POSIX refuses. The readers cannot fail at all — an unset variable is an answer, not an error.

The programs it starts

A script that drives other programs needs more than a system call: it wants to know what the program said, how it ended, and whether it is still running. The process unit’s second half is a start record and a child value.

cat > build.mica << 'EOF'
program Build;

imp
    WriteLn, ToString, Clear, Append : std;
    ProcessStart, Child, Reader, ArgumentList, WiringPipe, Spawn, Wait, ExitCode,
        Output, ReadLine, Close, Halt : process;
    OsError : posix;

const
    Tool = "/bin/echo";

var
    start     : ProcessStart;
    child     : Child;
    reader    : Reader;
    line      : stringbuffer;
    arguments : ArgumentList;
    reason    : OsError;
    more      : bool;

begin
    Append(address arguments, "compiling unit one");
    Append(address arguments, "and unit two");

    start.command   := Tool;
    start.arguments := arguments;
    start.output    := WiringPipe;

    child := Spawn(start) on fail reason do begin
        WriteLn("cannot start the tool");
        Halt(1);
    end;

    reader := Output(child);
    more := ReadLine(address reader, address line) on fail use False;

    while more do begin
        WriteLn("tool said: %ls", ToString(line));
        Clear(address line);
        more := ReadLine(address reader, address line) on fail use False;
    end;

    Wait(address child) on fail continue;
    WriteLn("the tool ended with %d", ExitCode(child));
    Close(address child);
end.
EOF

mica --run build.mica
tool said: compiling unit one and unit two
the tool ended with 0

ProcessStart is a plain record, so a start you leave alone behaves the way a shell would: inherited streams, inherited environment, the parent’s directory. Fill in what you want changed — WiringPipe on a stream to hold its other end, an environment list to hand the child its own, a directory to start it somewhere else.

VerbWhat it does
Spawn(start)starts it, searching PATH the way a shell does; fails with the reason it could not start
Wait(address child)blocks until it ends and records how; waiting twice is safe
Poll(address child)the same question without blocking — the supervisor’s road
Running, ExitCode, Signalwhat the last question learned
Terminate, Killask it to stop, or end it outright
WriteLine, CloseInputspeak into its input, then let it see the end
Output, Errors, ReadLineread its streams line by line
Close(address child)give back the pipe ends the parent still holds

A child that could not be started at all is different from one that started and failed: the first is a failure on your channel, the second is a status your child reports. A program that does not exist fails Spawn with Enoent, and one that exists but cannot be executed fails it with Eacces, before any child is there to wait for.

Watching what it started

A script that starts a program often wants to know it is still working — not merely still running. Those are different questions, and only the first one the kernel can answer. The second needs the child to say so, which is what a heartbeat is.

Mica writes that without a select, a timer callback or a signal handler. The child’s output is a stream, because a generator reading its lines is one; the deadline is a second generator joined into the same stream, so a missed beat is an ordinary value; and the policy is yours:

feed := schedule Beats(child) buffer 4;   { one value per line the worker sends }
schedule Ticks(budget) into feed;         { one value per budget that passes    }

watch : for arrival in feed do begin
    if arrival = BeatValue then
        misses := 0
    else begin
        misses := misses + 1;
        leave watch when misses >= 2;     { two ticks with no beat between them }
    end;
end;

Leaving that loop ends both generators and the child with them, because they belong to the activation that started them. The whole example — one file that starts itself as the worker, so both halves fit on one screen — ships as Supervisor.

One field on ProcessStart is worth knowing before you need it: ownGroup := True makes the child lead its own process group, so Terminate and Kill reach everything it started. Without it you end the program at the top and leave its helpers running — still holding the descriptors they inherited, which is why a pipe you expect to close stays open.

The flags it accepts

A script that grows past two arguments wants them named. The flags unit takes the declaration-first shape: the program says what it accepts, one Parse walks the command line, and the typed readers answer afterwards.

cat > serve.mica << 'EOF'
program Serve;

imp
    Write, WriteLn, Length : std;
    FlagSet, ArgumentList, FlagError, IntFlag, TextFlag, BoolFlag, Parse,
        IntValue, TextValue, BoolValue, Arguments, Help : flags;
    Halt : process;

const
    PortName    = "port";
    RootName    = "root";
    VerboseName = "verbose";

var
    cli    : FlagSet;
    rest   : ArgumentList;
    reason : FlagError;

begin
    IntFlag(address cli, PortName, 8080, "the port to listen on");
    TextFlag(address cli, RootName, ".", "the directory to serve");
    BoolFlag(address cli, VerboseName, False, "log every request");

    Parse(address cli) on fail reason do begin
        Write("%ls", Help(cli));
        Halt(2);
    end;

    rest := Arguments(cli);

    WriteLn("port=%lld", IntValue(cli, PortName));
    WriteLn("root=%ls", TextValue(cli, RootName));
    if BoolValue(cli, VerboseName) then WriteLn("verbose=on") else WriteLn("verbose=off");
    WriteLn("left over=%lld", Length(rest));
end.
EOF

mica --run serve.mica --port 9000 --verbose site/public notes.txt
port=9000
root=.
verbose=on
left over=2

The help text is never written — it is generated from the declarations, so a flag that exists is a flag that is documented. A command line the program cannot read fails on the language’s own channel, and the handler above is what a user sees:

mica --run serve.mica --prot 9000; echo "exit=$?"
  -port     int   the port to listen on (default 8080)
  -root     text  the directory to serve (default .)
  -verbose  bool  log every request (default false)
exit=2

The grammar is the one every Go program already speaks: -name value or -name=value, one dash or two, a bool set by being named and cleared only by -name=false, the flag section ending at the first argument that is not a flag or at a bare --. What is left over is Arguments, in the order it was typed.

VerbWhat it does
IntFlag, FloatFlag, TextFlag, BoolFlagdeclare a name, its type, its default and the line that explains it
Parse(address cli)read the program’s own command line; ParseList reads one you hand it
IntValue, FloatValue, TextValue, BoolValuewhat a flag holds — the command line’s value, or the default
WasGiven(cli, name)whether the command line named it, which a default cannot tell you
Arguments(cli)everything after the flags
Help(cli)the usage text, generated from the declarations

The set is a value the program declares, so a program can hold two command lines and a test can parse one it made up. Failures split the way the rest of Mica does: what the user typed reaches the failure channel (UnknownName, MissingValue, NotAnInteger, NotAFloat, NotABool), while what the program spelled — a name declared twice, or a value read at the wrong type — traps, because no default could hide that.

The file becomes a command

This is the part that surprises people meeting it for the first time: a Mica source file can be a command — indistinguishable from any installed program — and the mechanism behind it is worth two minutes of understanding, because it is the same mechanism behind every script on every Unix system.

What a shebang is. When you execute a file, the kernel reads its first two bytes. If they are #!, it treats the rest of that first line as the path of the program that should run this file, and starts that program with the file’s path appended. That is the whole mechanism. It is a kernel feature, not a language feature — it is how every bash and python3 script becomes runnable — and the line is called the shebang line.

#!/usr/bin/env mica adds one useful indirection: env looks up mica on your PATH and runs it. That is the portable idiom scripting ecosystems settled on, because the script then never hardcodes where the compiler is installed. Most languages need the GNU-only env -S trick here, to smuggle a flag like --run into the line; Mica does not, because the bare spelling mica tool.mica already means “run this” — and the compiler’s source preparation removes the #! line on every road, line numbers preserved, so the same file compiles through --compile, runs through --run, and opens in the editor without a diagnostic on line one.

A tool, step by step. Here is a complete command-line tool — addup, which adds up whatever numbers follow it. Step one, write the file:

#!/usr/bin/env mica
{
    addup — add up whatever numbers follow the command.
}
program AddUp;

imp
    WriteLn : std;
    *       : process;
    Val     : strings;

var
    total, number : int64;
    index         : int32;

begin
    total := 0;

    { every command-line argument is either a number that joins the total or a complaint on the way out }
    for index := 1 to ArgCount() do
        if Val(Arg(index), address number) then
            total := total + number
        else
            WriteLn("not a number: %ls", Arg(index));

    WriteLn("%lld", total);
end.

Step two, make it executable — once:

chmod +x addup.mica

Step three, it runs:

$ ./addup.mica 12 30
42

Step four — the full command experience. Commands do not carry file extensions, and the bare spelling accepts that too: any existing file counts as the script. So drop the extension and move it into a folder on your PATH — ~/bin, which Ubuntu’s login profile puts at the front of PATH from the next login on, and this shell after the export:

$ mkdir -p ~/bin
$ cp addup.mica ~/bin/addup
$ export PATH="$HOME/bin:$PATH"
$ addup 12 30 -5
37

addup is now a command. (A name of its own matters: sum is already a Linux command, the checksum tool, and the shell finds whichever comes first on the PATH.) Whoever runs it sees arguments in, answer out, exit code honest — and a civil reply instead of a stack trace when an argument is not a number:

$ addup 12 oops 30
not a number: oops
42

What they never see is that there is no interpreter behind it. Each invocation compiles the file into a temporary directory and runs the result — compile and run together, beneath the threshold anyone notices — and every compile-time check ran before the first statement did. Your ten-line tool was type-checked, format-string checked, and bounds-guarded on every single invocation. No scripting language gives you that; no compiled language made it this easy before.

And a here-document is a program — --run - reads the source from standard input:

mica --run - << 'EOF'
program Hi; imp WriteLn : std; begin WriteLn("hi from a here-document"); end.
EOF
hi from a here-document

Your libraries come along

If your script lives inside a tree that carries a mica.project, the run mode discovers it exactly the way the editor does — the nearest project file up the directory tree governs — and its encoding and contracts apply. A script beside your project imports your libraries with no flags at all:

$ cat mica.project
{
  "contracts": ["build-lib"]
}
$ mica --run twice.mica       # twice.mica says: imp Twice : Numbers;
twice(21) = 42

No import paths to configure, no environment to activate. The file your build and your editor already read is the whole configuration — and a here-document typed inside the tree gets the same facts.

The check happens before the run

Here is the part no scripting language gives you. Change args.mica to print a string with %s — the utf-8 specifier — while the file compiles under the utf-32 default:

     9: WriteLn("first=%s", Arg(1));
                            ^ analyzer error 5184 [9,25]: format string validation error: string format specifier at position 1 must use '%ls' for UTF-32 encoding (found '%s')

The script never ran. Elsewhere the matching mistake waits until run time, for the input that happens to reach the broken line; a Mica script fails on your terminal, before a single statement executes, with the line and the fix in the message. Every compile-time check — types, format strings, unused variables, definite assignment — runs at full strength under --run, because the default is the debug tier, where checks are maximal and latency is already unmeasurable.

The mode is deliberately narrow about everything else. It targets the host architecture, and it combines with eight flags — --optimize, --stdlib, --memory-class, --tasking, --processes, --runtime-flags, --stack-size and --remarks. Ask it for anything the build owns and it refuses in one line:

mica --build somewhere --run hello.mica
the flag '--build' cannot be combined with '--run': the run mode owns compilation, linking, and the build directory, and combines only with --optimize, --stdlib, --memory-class, --tasking, --processes, --runtime-flags, --stack-size, and --remarks
run 'mica --help' for the compiler's full flag surface

And when a script outgrows scripting — it happens — nothing needs rewriting: the same file goes through --compile --link --optimize O2 and becomes the shipped program it was slowly becoming anyway.

The command-line reference states every rule of the run mode.

Next