A running Mica program can answer questions about itself. Started with the
runtime flag rtp=listen, it opens a socket of its
own, and a client, such as the editor or a command-line tool, connects and asks.
The protocol is to a running program what the Language Server Protocol is to
source code: JSON-RPC 2.0 messages, framed the same way, in a session that
opens with initialize.
./server +mica rtp=suspend -mica --port 8080mica runtime protocol: the program waits for 'runtime/continue' on /run/user/1000/mica/48213.sockA program started without an rtp= flag has no agent at all: no thread, no
socket, and nothing checked while it runs.
This page is protocol version 1. The version changes when a method, a member or
a code changes meaning or disappears; methods and members are added within a
version, and the answer to initialize lists the methods a program offers.
The transport
| Rule | What it means |
|---|---|
| a Unix domain socket per program | only clients on the same machine connect; a client on another machine reaches a program through SSH forwarding of the socket |
| owner-only | the socket file allows only its owner (mode 0600), the directory of the default path allows only its owner (mode 0700), and a connecting client whose user differs from the program’s is closed before anything is read |
| the default path | /run/user/<uid>/mica/<pid>.sock when the system provides the user’s runtime directory, /tmp/mica-<uid>/<pid>.sock otherwise, so a client finds every program of its user in two directories; no environment variable is read |
| a named path | rtp=<path> listens at path instead; a socket left there by a program that ended without removing it is replaced, and a path another program listens on, or a file that is not a socket, is refused |
| removed at exit | the program removes its socket when it ends |
| one client at a time | a second client waits until the first leaves, and is then served a session of its own |
| the program undisturbed | the agent is one thread outside the program’s carriers, blocks every signal so the program’s own handlers receive them, and allocates no memory of the program’s |
Framing
Every message, in both directions, is a header block and a body, exactly as the Language Server Protocol frames them. Each header line ends with a carriage return and a line feed, and an empty line ends the block:
Content-Length: 76
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}| Header | Rule |
|---|---|
Content-Length | required: the body’s size in bytes, in decimal digits, at most 65536 |
Content-Type | accepted and not read |
Header names are compared without regard to case, and each is written as the name, a colon, one space and the value. A frame that breaks these rules, whether with a header of another name, a header named twice, a missing or malformed length, a body larger than the bound or a header block longer than 1024 bytes, ends the session without an answer, because the client’s next frame can no longer be found. The client may connect again.
The body is JSON as RFC 8259 defines it, read strictly: well-formed UTF-8, no comments, no trailing commas, no key named twice in one object (judged by the keys’ text, so a key spelled with an escape and the same key spelled without one are the same key), and no escaped NUL character in a string.
The session
A client’s first request is initialize. Any other request before it is
answered with the error -32002, and a second initialize in the same session
with -32600. Each connection is a session of its own, so a client that
reconnects initializes again.
A request carries jsonrpc (the string "2.0"), method, an id, and
optionally params, an object or an array; no other member is accepted. The
id is an integer, a string of at most 255 bytes, or null, and the answer
repeats it. A message without an id is a notification: it is carried out and
never answered, not even when it is refused. A batch, a JSON array of messages,
is not accepted.
Methods
initialize
Opens the session.
| Parameter | Type | Meaning |
|---|---|---|
protocolVersion | integer | the protocol version the client speaks; required |
The result:
{"protocolVersion":1,"runtime":{"name":"mica-runtime","processId":48213,"suspended":true},"methods":["initialize","runtime/manifest","runtime/configuration","runtime/snapshot","runtime/suspend","runtime/continue","tasks/tree","tasks/cancel","tasks/subscribe","tasks/unsubscribe","memory/regions","memory/blocks","memory/types"],"notifications":["tasks/changed"]}| Member | Meaning |
|---|---|
protocolVersion | the protocol version the program speaks |
runtime.name | always mica-runtime |
runtime.processId | the program’s process identifier |
runtime.suspended | whether the program is held before its first statement, waiting for runtime/continue |
methods | every method the program offers, in this page’s order |
notifications | every notification the program may send a subscribed client |
A version the program does not speak is answered with -32602, reason
protocol-version, and the versions it does speak in data.supported.
runtime/manifest
The program’s manifest, the document the
compiler wrote into the program file, as mica --inspect reads it; null for a
program linked without one. It takes no parameters.
runtime/configuration
What the program’s start resolved, the configuration member of the answer to
the info runtime flag, with one member only a
running program knows: carriers.started, the carriers its pool has started,
its main thread among them, 0 while the pool has not started, and 1 for a
single-carrier program. It counts the pool where a count of the process’s
threads cannot, under an emulator, whose own threads are the process’s as
well. It takes no parameters.
{"carriers":{"effective":2,"source":"build","oversubscribed":false,"started":2},"processes":{"effective":8,"source":"processors","oversubscribed":false},"stdout":"default"}tasks/tree
The tasks the scheduler holds, read while the program holds still (see
reading a running program); null for a program
without tasks. It takes no parameters.
{"flavor":"multicore","carriers":2,"complete":true,"tasks":[{"ordinal":0,"id":0,"kind":"main","state":"ready","parent":-1,"cancelled":false},{"ordinal":1,"id":3,"kind":"task","state":"running","parent":0,"cancelled":false},{"ordinal":2,"id":1,"kind":"task","state":"parked","parent":0,"cancelled":false}]}| Member | Meaning |
|---|---|
flavor | single or multicore, the program’s tasking flavor |
carriers | the carriers of a multicore program’s pool; 0 before the pool starts, at the program’s first task or suspension |
complete | false when the list names fewer tasks than the scheduler holds: at most 256 are named, and fewer when the answer would pass its bound |
tasks[].ordinal | the task’s place in this answer; ordinals may name other tasks in the next answer |
tasks[].id | the task’s identity for its whole life: 0 for the main body, then one number per task and generator in the order they begin; tasks/cancel names a task by it |
tasks[].kind | main for the program’s main body, task, or generator |
tasks[].state | running on a carrier, ready to run, parked waiting on a descriptor or a timer, suspended for a generator waiting between two pulls, finished |
tasks[].parent | the ordinal of the task that owns the scope the task was started in, or -1 |
tasks[].cancelled | whether a cancellation is pending for the task |
tasks[].owned_scope | for a task that owns a scope: live_children, error (the first failure’s code, 0 for none) and timed_out |
tasks[].generator | for a generator: started, exhausted, closing, and holder, the ordinal of the activation that holds the instance between two pulls, or -1 for a stream’s producer, which the stream’s ring holds, and for a holder the list does not name |
A generator its caller holds between two pulls is named with the state
suspended, and its generator.holder names the activation that holds it.
tasks/cancel
Cancels the scope a task was started in, through the scheduler’s own cancellation: the task and every task started beside it stop, and every cleanup of theirs runs, exactly as when the scope’s block is left early. The task that owns the scope is not cancelled; its join completes once the scope’s tasks have stopped.
| Parameter | Type | Meaning |
|---|---|---|
task | integer | the identity tasks/tree shows as id; required |
{"cancelled":true,"live":3}cancelled is true when this request cancelled the scope and false when a
cancellation of it was already under way; live is the number of tasks the
scope held. Tasks that are running or ready stop at their next suspension
point, and a task waiting on a descriptor or a timer is woken by the scope’s
owner once the owner waits in its join, at the end of the scope’s block. The main body was started in no scope and is
refused with reason main-body; an identity no living task has, with
unknown-task.
tasks/subscribe
Starts the notification tasks/changed, sent at most once per interval and only
when tasks began or ended since the last one. A second subscription changes the
interval. A program counts its tasks only while a client is subscribed.
| Parameter | Type | Meaning |
|---|---|---|
intervalMilliseconds | integer | the shortest time between two notifications, from 50 to 60000; required |
{"subscribed":true}The notification:
{"jsonrpc":"2.0","method":"tasks/changed","params":{"begun":3,"ended":0}}begun and ended count the tasks, a channel’s producers among them, that began
and ended since the last notification, so a client that missed none can add
them up. The subscription ends with tasks/unsubscribe or with the session.
tasks/unsubscribe
Ends the subscription. It takes no parameters.
{"subscribed":true}subscribed is true when there was a subscription to end.
runtime/snapshot
The runtime’s whole description of itself, read while the program holds still: the same document a debugger receives from the editor’s runtime view. It takes no parameters.
{"mica_narrator":1,"tasking":{...},"tape":null}tasking is the answer to tasks/tree, and tape the automatic
differentiation tape’s summary, or null for a program without one.
runtime/suspend
Stops the program until runtime/continue, or until the client that suspended
it leaves. Readings taken while it is suspended all describe the same instant.
It takes no parameters.
{"suspended":true}suspended is true when this request suspended the program and false when
it was suspended already.
runtime/continue
Releases a program held before its first statement by rtp=suspend, or
suspended by runtime/suspend. It takes no parameters.
{"released":true}released is true when this request released the program and false when
the program was neither held nor suspended.
memory/regions
What the program’s memory holds, at the coarsest grain: every live region with its blocks and its bytes, the deployment class the program was built for, the arena where it has one, and the explicit cells it holds. Read while the program holds still (see reading a running program). It takes no parameters.
{"sites":true,"class":"hosted-heap","arena":null,"explicit":{"cells":3,"bytes":96},"owners":[{"owner":"program","task":-1,"regions":[{"ordinal":0,"blocks":1,"dedicated":0,"capacity":65552,"used":1184}]}],"complete":true}| Member | Meaning |
|---|---|
sites | whether the program’s cells carry their allocating site, which only a debug-tier program’s do |
class | the deployment class: hosted-heap or fixed-arena |
arena | the fixed arena’s size and used bytes, or null for a program on the hosted heap |
explicit.cells, explicit.bytes | the cells new made outside any region, and their bytes |
owners[].owner | program for the regions every task shares, task for one task’s own partition, shared for the one partition every task of a program without the multicore runtime allocates from |
owners[].task | the task’s identity, or -1 where the partition belongs to no single task |
owners[].regions[] | each region’s ordinal, its blocks and dedicated block counts, and the capacity and used bytes of both together |
complete | false when the program holds more regions than the answer names |
Every tier answers this reading: a region’s blocks and its bump are the region’s own geometry, which needs no header.
memory/blocks
Every block of every live region, walked cell by cell. Read while the program holds still. It takes no parameters.
{"sites":true,"blocks":[{"owner":"program","task":-1,"region":0,"kind":"regular","capacity":65552,"used":1184,"cells":[{"offset":0,"bytes":32,"state":"live","type":"Ledger","file":"/home/mica/Ledger.mica","line":79,"column":15},{"offset":32,"bytes":528,"state":"live"},{"offset":560,"bytes":128,"state":"free"},{"offset":688,"bytes":128,"state":"live"},{"offset":816,"bytes":368,"state":"table"}]}],"complete":true}| Member | Meaning |
|---|---|
blocks[].kind | regular for a block the region bumps in, dedicated for one holding a single oversize cell |
blocks[].capacity, blocks[].used | the block’s payload bytes, and the bytes its cells occupy |
cells[].offset, cells[].bytes | where the cell stands in the block’s payload, and how far the next cell stands, its header included |
cells[].state | live for a value’s storage, free for a cell released onto its class’s free list, table for the region’s own table of those lists |
cells[].type | the allocated type’s name, absent for a cell made by code compiled at another tier |
cells[].file, line, column | where the allocation stands in the program’s source, absent with the type |
complete | false when a block holds more cells than the answer names, or a walk met a cell its span could not confirm |
The walk is the cell header’s own: each cell says how far the next one stands.
A program built at a release tier carries no header, and this reading is
refused with -32600, reason needs-debug.
memory/types
The program’s cells summed by the type they hold: every live region cell and every explicit cell, released cells left out. Read while the program holds still. It takes no parameters.
{"sites":true,"types":[{"cells":1,"bytes":32,"type":"Ledger","file":"/home/mica/Ledger.mica","line":79,"column":15},{"cells":3,"bytes":96,"type":"Node"},{"cells":2,"bytes":656}],"complete":true}| Member | Meaning |
|---|---|
types[].type | the allocated type’s name, absent for cells made by code compiled at another tier |
types[].cells, types[].bytes | how many cells of the type the program holds, and their bytes |
types[].file, line, column | where the allocation stands, written only where one allocation in the program makes this type; a type made in several places carries its name alone |
complete | false when the program holds more types than the answer names |
The entries are ordered by the type’s name, the nameless entry last, so one
program’s reading is one document however its cells were met. Like
memory/blocks, this reading needs the debug tier’s header and is otherwise
refused with reason needs-debug.
Reading a running program
tasks/tree, tasks/cancel, runtime/snapshot, runtime/suspend and the three
memory/ readings stop the program while they read, the way a debugger stops it: every thread of the program is sent the
signal SIGURG, and each waits in the signal’s handler until the reading is
done. A reading holds the program for a few milliseconds: the time its threads
take to stop and the answer takes to render. Because a signal reaches a thread wherever it is, a program
holds still for a reading even inside a long loop or a C call.
| What a program may notice | Why |
|---|---|
a system call that the kernel does not restart returns early with EINTR | the signal interrupts it; reads and writes are restarted, while a sleep or a wait with a timeout may return early, as under Go’s preemption signal |
a SIGURG handler the program installed before the agent started still receives every SIGURG that is not the agent’s | the agent passes them on |
a SIGURG handler the program installs after the agent started replaces the agent’s | the readings then fail with stop-timeout |
Errors
A refused request is answered with a JSON-RPC error. Its data.reason names
the refusal in one word a client can act on.
{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"the message is not a request the protocol accepts","data":{"reason":"member"}}}| Code | Message | Reasons |
|---|---|---|
-32700 | the message is not well-formed JSON | json |
-32600 | the message is not a request the protocol accepts | batch, not-object, jsonrpc-version, id, method, params, member, initialized-already, needs-debug |
-32601 | the runtime offers no such method | method |
-32602 | the parameters do not fit the method | parameter, protocol-version, unknown-task, main-body, no-tasks |
-32603 | the runtime could not answer | answer-too-large, stop-timeout |
-32002 | the session is not initialized | not-initialized |
| Reason | The request |
|---|---|
json | is not JSON the protocol reads; data.detail says why and data.position is the byte offset in the body where reading stopped |
batch | is a JSON array |
not-object | is a JSON value other than an object |
jsonrpc-version | has no jsonrpc member, or one that is not "2.0" |
id | has an id that is not an integer, a string within the bound, or null; the answer’s id is null |
method | has no method string, or names a method the program does not offer |
params | has params that are neither an object nor an array |
member | holds a member JSON-RPC does not define |
initialized-already | is an initialize in a session that is open already |
not-initialized | came before initialize |
parameter | lacks a parameter the method needs, or has one of another type |
protocol-version | names a protocol version the program does not speak; data.supported lists those it does |
answer-too-large | would be answered with more than 65536 bytes |
needs-debug | asks for a reading that needs the cell headers only a program built at the debug tier carries |
stop-timeout | needed the program to hold still, and a thread of it did not stop within 2 seconds |
unknown-task | names a task identity no living task has |
main-body | asks to cancel the scope of the main body, which was started in no scope |
no-tasks | asks about tasks in a program built without the task runtime |
A body that is not JSON, and a request whose id cannot be read, are answered
with "id":null.
When the agent cannot start
The start ends with one line on standard error and exit status 2, before any of the program runs:
mica runtime flags: the runtime protocol could not start on '<path>': <reason>| Reason |
|---|
| the socket path is longer than a Unix domain socket address holds |
| the socket directory is not a directory only this user can enter |
| the socket directory could not be created |
| another process listens on the socket path, or the path is not a socket |
| the socket could not be opened |
| the agent thread could not be started |
A reason that comes from the operating system is followed by its own words, for
example : Permission denied.
Bounds
| Bound | Value |
|---|---|
| a request body | 65536 bytes |
| a header block | 1024 bytes |
| the tokens of a request’s JSON | 1024 |
| the nesting of a request’s JSON | 16 containers |
a string id | 255 bytes |
| an answer body | 65536 bytes |
| the tasks one answer names | 256 |
| the wait for a program to hold still | 2 seconds |
| a send to a client that does not read | 1 second, after which the session ends |
| clients the listen queue holds while one is served | 4, and the kernel may admit one more |