Most languages make you choose. Write the server the readable way — accept a connection, handle it, accept the next — and it serves one client at a time. Write it the scalable way and the readable version is gone: callbacks, promises, an event loop, a state machine where a straight line used to be.

Mica does not have that trade, because the thing that made it necessary is not in the language. A wait here suspends one task, not the program. So the readable spelling is the scalable one:

concurrent
    repeat
        accepted := Accept(listener) on fail continue;
        schedule Session(accepted);
    until False;
end;

That is a server. Every connection gets a task, every task waits at its own socket, and the carrier runs whoever is ready.

The example is examples/EchoServer.

Listening

imp
    Listener, Connection, ListenTcp, Port, Accept, DialTcp, Close, CloseListener : net;
    WriteLine, ReadLine : net;
    OsError : posix;

listener := ListenTcp(0) on fail leave;
port := Port(listener) on fail leave;

ListenTcp answers a Listener bound on every interface of the machine. Passing port zero asks the operating system to choose a free port, and Port reads back which one it chose — the idiom that keeps a program from ever colliding with something else on a fixed number.

Both calls are fallible, and their reasons are the ones you already know from files: Eaddrinuse when another program holds the port, Eacces for a privileged one. There is no separate network error vocabulary to learn.

CloseListener gives the descriptor back. Like Close on a file it is nil-safe and repeatable, so it is the partner of defer on every path out:

defer CloseListener(address listener);

Accepting, and what parks

accepted := Accept(listener) on fail leave;

While no connection is waiting, this call parks: it suspends the calling task and nothing else. Other tasks keep running, and the arriving connection wakes this one. The same is true of every transfer below — the dial, the reads, the writes.

That single property is what removes the trade at the top of this page. You are not paying for a thread per connection (a task’s stack is not an OS thread), and you are not writing a callback to avoid paying for one.

A scope’s deadline reaches a parked task exactly as it reaches any other, so an unbounded accept loop still ends when its scope does:

concurrent
    ScopeDeadline(5000);                 { the whole server, five seconds }

    repeat
        accepted := Accept(listener) on fail continue;
        schedule Session(accepted);
    until False;
end;

When the deadline fires, the parked Accept is abandoned and the block moves on. You do not check a flag; the loop simply ends.

A connection is a value

task Session(c : Connection);

The accepted connection travels into its session by value — the task gets its own copy, deep-copied when you schedule it, exactly as any other value would be. Two consequences, both good:

  • the accept loop can reuse its own variable on the very next turn, because the session is not looking at it;
  • no two tasks can ever name one connection by accident.

This is not a network feature. It is Mica’s ordinary copy semantics, applied to a record that happens to hold a socket.

Reading lines

task Session(c : Connection);
var
    text : stringbuffer;
    more : bool;
begin
    more := ReadLine(address c, address text) on fail use False;

    while more do
    begin
        WriteLine(address c, ToString(text)) on fail continue;
        Clear(address text);
        more := ReadLine(address c, address text) on fail use False;
    end;

    Close(address c);
end;

ReadLine appends into a stringbuffer you own — one builder serves a whole conversation, so a busy session allocates nothing in its steady state — and it answers False once the peer has hung up with nothing left. That is why the session loop needs no other end condition.

A socket delivers bytes, not lines, and the unit takes the whole of that problem off your desk:

what arriveswhat you read
a line split across several receivesone line
a code point split across two receivesone character
CRLF line endings, even splitone line, no trailing return
a lone carriage returncontent, kept
a last line with no terminatorone line

Text crosses the wire as UTF-8 whichever encoding your program was compiled for, so a --platform ...,utf-32 program and a --platform ...,utf-8 program read each other exactly.

WriteLine puts the text and its terminator on the wire as one transfer — so two tasks writing to the same connection can never splice their lines together.

Bytes, when you have a protocol of your own

WriteBytes(address c, address frame[0], 12) on fail leave;
taken := ReadFully(address c, address frame[0], 12) on fail leave;

WriteBytes finishes what it starts: a stream socket may take fewer bytes than you offered, and completing the transfer is the library’s job, not yours — which is why nothing in this surface hands you a partial-write count to check.

For reading you choose your semantics:

  • ReadBytes answers with whatever has arrived, up to the count you asked for;
  • ReadFully assembles the whole frame, and answers short only when the peer closed first — which is how you tell an ended stream from a truncated frame.

Addresses and names

c := DialTcp("127.0.0.1", port) on fail leave;              { a literal }
peer := Resolve("example.org") on fail leave;               { a name }
c := DialTcpAt(peer, 443) on fail leave;

DialTcp takes a dotted-quad address; a spelling that is not one fails Einval. Names go through Resolve, which is a separate call on purpose: it is the only entry in the unit that waits without parking, because POSIX name resolution has no non-blocking form. Keeping it visible means a program that dials a literal address never pays for a facility it does not use, and a program that resolves knows exactly where it can stall.

ParseAddress and AddressText convert between the two spellings, and Loopback() answers this machine’s own address.

Failures are the ones you already know

Everything environmental arrives on the failure channel as an OsError:

Econnrefusednothing is listening on that port
Etimedoutnothing answered at all
Eaddrinuseanother program holds the port
Epipethe peer is gone
Econnresetthe peer vanished instead of closing
Ebadfyou already closed this connection

That last one is worth a sentence. Reading from a closed file traps, because a file handle is your program’s private possession and using a closed one is a program error. A connection is different: its other end can end it at any moment, so a network program is already written to answer failures on every transfer — and a reason it can handle beats a trap it cannot.

IsOpen and IsListening answer softly and never fail, and a Connection you never dialed reads as closed rather than as descriptor zero.

Many carriers

Nothing above changes when you compile the same source with

mica --tasking multicore,4 ...

The accept loop still parks, the sessions still run, and the counts still come out exact — a shared counter is exact because the statement that touches it is synchronized, which is the same rule that governs every other shared cell in the language. What multicore adds is that sessions genuinely run at the same instant, on different cores.

What is not here yet

IPv6, UDP, and Unix-domain sockets are not in this first cut, and neither is TLS. Descriptor(c) and ListenerDescriptor(l) hand you the raw socket for anything the curated surface does not carry yet, so nothing is a dead end.