Sigil Standard Library
Overview
The Sigil standard library provides core utility functions and predicates for common programming tasks. All functions follow canonical form principles - exactly ONE way to solve each problem.
Current Status
Implemented:
- ✅ Decode / validation pipeline for trusted internal data -
stdlib/decode - ✅ List predicates (validation, checking) -
stdlib/list - ✅ Numeric predicates and ranges -
stdlib/numeric - ✅ List utilities (head, tail, take/drop/reverse, safe lookup) -
stdlib/list - ✅ String operations (manipulation, searching) -
stdlib/string - ✅ String predicates (prefix/suffix checking) -
stdlib/string - ✅ File system operations -
stdlib/file - ✅ Filesystem watch streams -
stdlib/fsWatch - ✅ Process execution for harnesses and tooling -
stdlib/process - ✅ Canonical topology-backed relational database access -
stdlib/sql - ✅ PTY-backed interactive sessions -
stdlib/pty - ✅ Random number generation and collection helpers -
stdlib/random - ✅ Regular-expression compile/test/search with all-matches support -
stdlib/regex - ✅ Float arithmetic and math functions -
stdlib/float - ✅ Cryptographic hashing and encoding -
stdlib/crypto - ✅ HTTP and TCP clients and servers -
stdlib/httpClient,stdlib/httpServer,stdlib/tcpClient,stdlib/tcpServer - ✅ WebSocket servers and route-scoped text streams -
stdlib/websocket - ✅ Runtime dependency topology -
stdlib/topology - ✅ Runtime dependency config helpers -
stdlib/config - ✅ JSON parsing/serialization -
stdlib/json - ✅ Path manipulation -
stdlib/path - ✅ Pull-based event sources -
stdlib/stream - ✅ Time parsing/comparison/clock -
stdlib/time - ✅ Terminal raw-mode input and cursor control -
stdlib/terminal - ✅ URL parsing/query helpers -
stdlib/url - ✅ Deterministic feature-flag evaluation -
stdlib/featureFlags - ✅ Core prelude vocabulary (Option, Result) -
core/prelude(implicit) - ✅ Length operator (
#) - works on strings, lists, and maps
Rooted Module Syntax
e console:{log:λ(String)=>!Log Unit}
λmain()=>!Log Unit=console.log(§string.intToString(#[
1,
2,
3
])
++" "
++§time.formatIso(§time.fromEpochMillis(0)))
Design: Sigil writes rooted references directly at the use site. There are no import declarations, no selective imports, and no aliases. FFI still uses e module::path; Sigil modules use roots like §, •, ¶, ¤, †, ※, and ☴, while project-defined types and project sum constructors use µ.
Length Operator (#)
The # operator is a built-in language operator that returns the length of strings, lists, and maps.
Syntax:
#expression => Int
Type Checking:
- Works on strings (
String), lists ([T]), and maps ({K↦V}) - Compile error for other types
- Always returns integer (
Int)
Examples:
λmain()=>Bool=#"hello"=5
and #""=0
and #[
1,
2,
3
]=3
and #{
"a"↦1,
"b"↦2
}=2
Note on Empty Lists: Empty lists [] infer their type from context:
- In pattern matching: First arm establishes the type
- In function return: Return type annotation provides context
- In standalone expressions: Type cannot be inferred (use function with explicit return type)
Why # instead of functions?
- ONE canonical form - Not
§stringhelper calls vs§listhelper calls, just# - Leverages bidirectional type checking - Type is known at compile time
- Concise - Machine-first language optimizes for brevity (
#svslen(s)) - Zero syntactic variation - Single way to express "get length"
Codegen:
#s => (await s).length
#[1,2,3] => (await [1,2,3]).length
#{"a"↦1} => (await new Map([["a",1]])).size
Note: The deprecated §list.len function has been removed. Use # instead.
Module Exports
Sigil uses file-based visibility:
.lib.sigilexports all top-level declarations automatically.sigilfiles are executable-oriented
There is no export keyword.
Feature Flags
§featureFlags is the canonical typed evaluation surface for first-class featureFlag declarations.
Current public types:
t Config[T,C]={key:Option[λ(C)=>Option[String]],rules:[Rule[T,C]]}
t Entry[C]
t Flag[T]={createdAt:String,default:T,id:String}
t RolloutPlan[T]={percentage:Int,variants:[WeightedValue[T]]}
t Rule[T,C]={action:RuleAction[T],predicate:λ(C)=>Bool}
t RuleAction[T]=Rollout(RolloutPlan[T])|Value(T)
t Set[C]=[Entry[C]]
t WeightedValue[T]={value:T,weight:Int}
λentry[C,T](config:Config[T,C],flag:Flag[T])=>Entry[C]
λget[C,T](context:C,flag:Flag[T],set:Set[C])=>T
Canonical usage:
§featureFlags.get(
context,
☴featureFlagStorefrontFlags::flags.NewCheckout,
•config.flags
)
Current §featureFlags.get precedence is:
- first matching rule wins
Value(...)returns its value immediatelyRollout(...)deterministically buckets with the resolved key- if no rule matches, return the declaration
default
Entry[C] and Set[C] let one config snapshot hold multiple flag value types while keeping the context type explicit.
File, FsWatch, Path, Process, Pty, Stream, WebSocket, Random, JSON, Time, and URL
§file exposes canonical UTF-8 filesystem helpers:
λmain()=>!Fs String={
l out=(§path.join(
"/tmp",
"sigil.txt"
):String);
l _=(§file.writeText(
"hello",
out
):Unit);
§file.readText(out)
}
It also exposes makeTempDir(prefix) for canonical temp workspace creation in tooling and harness code.
For topology-aware projects with labelled boundary handling, the named-boundary surface is:
appendTextAtexistsAtlistDirAtmakeDirAtmakeDirsAtmakeTempDirAtreadTextAtremoveAtremoveTreeAtwriteTextAt
Those functions take a §topology.FsRoot handle so policies can target exact filesystem roots.
§fsWatch exposes canonical advisory recursive filesystem watches backed by §stream:
t Event=Changed(String)|Created(String)|Removed(String)
t Watch={id:String}
λclose(watch:Watch)=>!FsWatch Unit
λevents(watch:Watch)=>!FsWatch §stream.Source[Event]
λwatch(path:String)=>!FsWatch Owned[Watch]
λwatchAt(path:String,root:§topology.FsRoot)=>!FsWatch Owned[Watch]
FsWatch rules:
- watches are recursive in v1
- emitted paths are relative to the watched directory
- events are advisory; duplicate or coalesced delivery is allowed
watchandwatchAtreturn owned watch handles and are intended to be used withusingwatchAtis the named-boundary variant for topology-aware projects and takes a§topology.FsRoot- rename detection is not modeled separately in v1
§path exposes canonical filesystem path operations:
λmain()=>[String]=[
§path.basename("website/articles/hello.md"),
§path.join(
"website",
"articles"
)
]
§cli is the canonical typed CLI layer above §process.argv():
t Program[T]
t RootCommand[T]
t Command[T]
t Arg[A]
λprogram[T](description:String,name:String,root:Option[RootCommand[T]],subcommands:[Command[T]])=>Program[T]
λrun[T](argv:[String],program:Program[T])=>!Log!Process T
λroot0[T](description:String,result:T)=>RootCommand[T]
λroot1[A,T](arg1:Arg[A],build:λ(A)=>T,description:String)=>RootCommand[T]
λroot2[A,B,T](arg1:Arg[A],arg2:Arg[B],build:λ(A,B)=>T,description:String)=>RootCommand[T]
λroot3[A,B,C,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],build:λ(A,B,C)=>T,description:String)=>RootCommand[T]
λroot4[A,B,C,D,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],build:λ(A,B,C,D)=>T,description:String)=>RootCommand[T]
λroot5[A,B,C,D,X,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[X],build:λ(A,B,C,D,X)=>T,description:String)=>RootCommand[T]
λroot6[A,B,C,D,X,Y,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[X],arg6:Arg[Y],build:λ(A,B,C,D,X,Y)=>T,description:String)=>RootCommand[T]
λcommand0[T](description:String,name:String,result:T)=>Command[T]
λcommand1[A,T](arg1:Arg[A],build:λ(A)=>T,description:String,name:String)=>Command[T]
λcommand2[A,B,T](arg1:Arg[A],arg2:Arg[B],build:λ(A,B)=>T,description:String,name:String)=>Command[T]
λcommand3[A,B,C,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],build:λ(A,B,C)=>T,description:String,name:String)=>Command[T]
λcommand4[A,B,C,D,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],build:λ(A,B,C,D)=>T,description:String,name:String)=>Command[T]
λcommand5[A,B,C,D,X,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[X],build:λ(A,B,C,D,X)=>T,description:String,name:String)=>Command[T]
λcommand6[A,B,C,D,X,Y,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[X],arg6:Arg[Y],build:λ(A,B,C,D,X,Y)=>T,description:String,name:String)=>Command[T]
λflag(description:String,long:String,short:Option[String])=>Arg[Bool]
λoption(description:String,long:String,short:Option[String],valueName:String)=>Arg[Option[String]]
λrequiredOption(description:String,long:String,short:Option[String],valueName:String)=>Arg[String]
λmanyOption(description:String,long:String,short:Option[String],valueName:String)=>Arg[[String]]
λpositional(description:String,name:String)=>Arg[String]
λoptionalPositional(description:String,name:String)=>Arg[Option[String]]
λmanyPositionals(description:String,name:String)=>Arg[[String]]
λmain()=>!Log!Process String={
l command=(§cli.run(
§process.argv(),
§cli.program(
"Parse a small canonical CLI surface.",
"cliBasics",
Some(§cli.root1(
§cli.manyOption(
"Select a check id.",
"check",
Some("c"),
"ID"
),
λ(checks:[String])=>[String]=checks,
"Accept repeated check ids."
)),
[]
)
):[String]);
§string.intToString(#command)
}
CLI rules:
§process.argv()remains the only raw argv surface§cli.runprints canonical help on--help/-hand exits0§cli.runprints canonical parse errors plus usage/help and exits2- v1 supports one subcommand layer, long/short options,
--name=value,--, and trailing variadic positionals - argument values stay string-based in v1; domain conversion remains app logic after
runreturns
§process exposes canonical argv-based child-process execution:
λmain()=>!Process Unit={
l result=(§process.run(§process.command([
"git",
"status"
])):§process.ProcessResult);
match result.code=0{
true=>()|
false=>()
}
}
The canonical process surface is:
commandexitwithCwdwithEnvrunrunAtrunCheckedrunJsonstartstartAtwaitkill
Commands are argv-based only. Non-zero exit status is returned in ProcessResult.code; it is not a separate failure channel. When a caller wants checked failure semantics, use:
runChecked(command)=>Result[ProcessResult,ProcessFailure]runJson(command)=>Result[§json.JsonValue,ProcessFailure]
start and startAt return owned process handles and are intended to be used with using.
exit(code) terminates the current process and has type !Process Never.
§sql is the canonical relational database surface. The portable path is a typed Sigil-owned subset that stays stable across SQLite and Postgres through §topology.SqlHandle; backend binding happens in †runtime.withSqlHandles(...) and config worlds, not in app code.
t Bytes={base64:String}
t Column[Row,A]
t Delete[Row]
t Direction=Asc()|Desc()
t Insert[Row]
t Predicate[Row]
t RawRow={String↦Value}
t RawStatement
t Select[Row]
t SqlFailure={kind:SqlFailureKind,message:String}
t SqlFailureKind=Connection()|Constraint()|Decode()|Denied()|InvalidQuery()|MissingHandle()|Transaction()|Unsupported()
t Table[Row]
t Transaction={id:String}
t Update[Row]
t Value=BoolValue(Bool)|BytesValue(Bytes)|FloatValue(Float)|IntValue(Int)|NullValue()|TextValue(String)
λall[Row](handle:§topology.SqlHandle,select:Select[Row])=>!Sql Result[[Row],SqlFailure]
λallIn[Row](select:Select[Row],transaction:Transaction)=>!Sql Result[[Row],SqlFailure]
λand[Row](left:Predicate[Row],right:Predicate[Row])=>Predicate[Row]
λbegin(handle:§topology.SqlHandle)=>!Sql Result[Owned[Transaction],SqlFailure]
λboolColumn[Row](field:String,name:String)=>Column[Row,Bool]
λbytes(base64:String)=>Bytes
λbytesColumn[Row](field:String,name:String)=>Column[Row,Bytes]
λcommit(transaction:Transaction)=>!Sql Result[Unit,SqlFailure]
λdelete[Row](table:Table[Row])=>Delete[Row]
λdeleteWhere[Row](predicate:Predicate[Row],statement:Delete[Row])=>Delete[Row]
λeq[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λexecDelete[Row](handle:§topology.SqlHandle,statement:Delete[Row])=>!Sql Result[Int,SqlFailure]
λexecDeleteIn[Row](statement:Delete[Row],transaction:Transaction)=>!Sql Result[Int,SqlFailure]
λexecInsert[Row](handle:§topology.SqlHandle,statement:Insert[Row])=>!Sql Result[Int,SqlFailure]
λexecInsertIn[Row](statement:Insert[Row],transaction:Transaction)=>!Sql Result[Int,SqlFailure]
λexecUpdate[Row](handle:§topology.SqlHandle,statement:Update[Row])=>!Sql Result[Int,SqlFailure]
λexecUpdateIn[Row](statement:Update[Row],transaction:Transaction)=>!Sql Result[Int,SqlFailure]
λfloatColumn[Row](field:String,name:String)=>Column[Row,Float]
λgt[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λgte[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λinsert[Row](row:Row,table:Table[Row])=>Insert[Row]
λintColumn[Row](field:String,name:String)=>Column[Row,Int]
λlimit[Row](count:Int,select:Select[Row])=>Select[Row]
λlt[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λlte[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λneq[Row,A](column:Column[Row,A],value:A)=>Predicate[Row]
λnot[Row](predicate:Predicate[Row])=>Predicate[Row]
λnullable[Row,A](column:Column[Row,A])=>Column[Row,Option[A]]
λone[Row](handle:§topology.SqlHandle,select:Select[Row])=>!Sql Result[Option[Row],SqlFailure]
λoneIn[Row](select:Select[Row],transaction:Transaction)=>!Sql Result[Option[Row],SqlFailure]
λor[Row](left:Predicate[Row],right:Predicate[Row])=>Predicate[Row]
λorderBy[Row,A](column:Column[Row,A],direction:Direction,select:Select[Row])=>Select[Row]
λraw(params:{String↦Value},sql:String)=>RawStatement
λrawExec(handle:§topology.SqlHandle,statement:RawStatement)=>!Sql Result[Int,SqlFailure]
λrawExecIn(statement:RawStatement,transaction:Transaction)=>!Sql Result[Int,SqlFailure]
λrawQuery(handle:§topology.SqlHandle,statement:RawStatement)=>!Sql Result[[RawRow],SqlFailure]
λrawQueryIn(statement:RawStatement,transaction:Transaction)=>!Sql Result[[RawRow],SqlFailure]
λrawQueryOne(handle:§topology.SqlHandle,statement:RawStatement)=>!Sql Result[Option[RawRow],SqlFailure]
λrawQueryOneIn(statement:RawStatement,transaction:Transaction)=>!Sql Result[Option[RawRow],SqlFailure]
λrollback(transaction:Transaction)=>!Sql Result[Unit,SqlFailure]
λselect[Row](table:Table[Row])=>Select[Row]
λset[Row,A](column:Column[Row,A],statement:Update[Row],value:A)=>Update[Row]
λtable1[Row,A](column1:Column[Row,A],name:String)=>Table[Row]
λtable2[Row,A,B](column1:Column[Row,A],column2:Column[Row,B],name:String)=>Table[Row]
λtable3[Row,A,B,C](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],name:String)=>Table[Row]
λtable4[Row,A,B,C,D](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],column4:Column[Row,D],name:String)=>Table[Row]
λtable5[Row,A,B,C,D,E](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],column4:Column[Row,D],column5:Column[Row,E],name:String)=>Table[Row]
λtable6[Row,A,B,C,D,E,F](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],column4:Column[Row,D],column5:Column[Row,E],column6:Column[Row,F],name:String)=>Table[Row]
λtable7[Row,A,B,C,D,E,F,G](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],column4:Column[Row,D],column5:Column[Row,E],column6:Column[Row,F],column7:Column[Row,G],name:String)=>Table[Row]
λtable8[Row,A,B,C,D,E,F,G,H](column1:Column[Row,A],column2:Column[Row,B],column3:Column[Row,C],column4:Column[Row,D],column5:Column[Row,E],column6:Column[Row,F],column7:Column[Row,G],column8:Column[Row,H],name:String)=>Table[Row]
λtextColumn[Row](field:String,name:String)=>Column[Row,String]
λupdate[Row](table:Table[Row])=>Update[Row]
λupdateWhere[Row](predicate:Predicate[Row],statement:Update[Row])=>Update[Row]
SQL rules:
- the portable path is intentionally smaller than SQL itself: one-table full-row
select,insert,update,delete, predicates, ordering, limit, and transactions beginreturns an owned transaction handle and is intended to be used withusing- leaving a
using transaction=...scope withoutcommitrolls the transaction back - portable app code stays backend-neutral by targeting
§topology.SqlHandle; config chooses SQLite or Postgres §sql.raw...is the only blessed escape hatch for backend-specific features- raw statements use named parameters written as
:name; the runtime rewrites placeholders for each backend - raw SQL is non-portable by definition even though parameter binding remains canonical
- v1 portable
Valueis limited toBool,Int,Float,Text,Bytes, andNull
§pty exposes canonical interactive PTY sessions backed by §stream:
t Event=Output(String)|Exit(Int)
t Session={pid:Int}
t SessionRef={id:String}
t Spawn={argv:[String],cols:Int,cwd:Option[String],env:{String↦String},rows:Int}
λclose(session:Session)=>!Pty Unit
λcloseManaged(session:SessionRef)=>!Pty Unit
λevents(session:Session)=>!Pty §stream.Source[Event]
λeventsManaged(session:SessionRef)=>!Pty Owned[§stream.Source[Event]]
λresize(cols:Int,rows:Int,session:Session)=>!Pty Unit
λresizeManaged(cols:Int,rows:Int,session:SessionRef)=>!Pty Unit
λspawn(request:Spawn)=>!Pty Owned[Session]
λspawnManaged(request:Spawn)=>!Pty SessionRef
λspawnAt(handle:§topology.PtyHandle,request:Spawn)=>!Pty Owned[Session]
λspawnManagedAt(handle:§topology.PtyHandle,request:Spawn)=>!Pty SessionRef
λwait(session:Session)=>!Pty Int
λwaitManaged(session:SessionRef)=>!Pty Int
λwrite(input:String,session:Session)=>!Pty Unit
λwriteManaged(input:String,session:SessionRef)=>!Pty Unit
PTY rules:
eventsexposes one combined terminal streamOutput(text)carries terminal chunks in arrival orderExit(code)is emitted once when the session terminateswaitresolves to the same exit code reported by the sessionspawnandspawnAtreturn owned session handles and are intended to be used withusingspawnManagedandspawnManagedAtreturn storable runtime-managed session refs for long-lived server stateeventsManagedreturns an owned subscription stream for one managed session refwaitManagedreturns the exit code but leaves the managed ref open untilcloseManagedcloseManagedis idempotentspawnAtis the named-boundary variant for topology-aware projects and takes a§topology.PtyHandlespawnManagedAtis the named-boundary managed-ref variant for topology-aware projects and takes a§topology.PtyHandle
§stream exposes canonical pull-based runtime event sources:
t Hub[T]=StreamHub(Int)
t Next[T]=Done()|Item(T)
t Source[T]=StreamSource(Int)
λclose[T](source:Source[T])=>!Stream Unit
λhub[T]()=>!Stream Owned[Hub[T]]
λnext[T](source:Source[T])=>!Stream Next[T]
λpublish[T](hub:Hub[T],value:T)=>!Stream Unit
λsubscribe[T](hub:Hub[T])=>!Stream Owned[Source[T]]
Stream rules:
Source[T]is the canonical handle returned by stream-backed runtime APIsHub[T]is the canonical fanout surface for long-running app event distributionnextyieldsItem(value)while values remain andDone()when the source is exhaustedcloseis idempotent- after
close, subsequentnextcalls returnDone() hubandsubscribereturn owned handles and are intended to be used withusingpublishfanouts to current subscribers in send order- generic stream failure is not modeled in
§stream; producer APIs own their error events §streamis intentionally small and does not expose combinator-style operator families
§websocket exposes canonical server-first WebSocket handling backed by §stream:
t Client={id:String}
t Route={handle:§topology.WebSocketHandle,path:String}
t Server={port:Int}
λclose(client:Client)=>!WebSocket Unit
λconnections(handle:§topology.WebSocketHandle,server:Server)=>!WebSocket Owned[§stream.Source[Client]]
λlisten(port:Int,routes:[Route])=>!WebSocket Owned[Server]
λmessages(client:Client)=>!WebSocket Owned[§stream.Source[String]]
λport(server:Server)=>Int
λroute(handle:§topology.WebSocketHandle,path:String)=>Route
λsend(client:Client,text:String)=>!WebSocket Unit
λwait(server:Server)=>!WebSocket Unit
WebSocket rules:
listenbinds one port plus an exact-path route list- route paths must be unique within one server
- route handles must be unique within one server
connectionsyields accepted clients for one exact§topology.WebSocketHandlemessagesyields text frames for one clientlisten,connections, andmessagesreturn owned handles and are intended to be used withusingsendwrites one text frame to one clientclosecloses one client connection- v1 is server-only; there is no WebSocket client API, binary-frame surface, or broadcast helper
runAt and startAt are the named-boundary variants for topology-aware projects. They take a Command plus a §topology.ProcessHandle.
§log is the named-boundary logging surface:
λmain()=>!Log Unit=§log.write(
"customer created",
•topology.auditLog
)
It currently exposes:
write
Projects can keep using §io for ordinary textual output, but labelled boundary rules target §log.write because it names the sink explicitly.
§random exposes the canonical runtime random surface:
λmain()=>!Random Unit={
l _=(§random.intBetween(
6,
1
):Int);
l deck=(§random.shuffle([
"orc",
"slime",
"bat"
]):[String]);
l _=(§random.pick(deck):Option[String]);
()
}
The canonical random surface is:
intBetweenpickshuffle
Randomness is world-driven through †random.real(), †random.seeded(seed), and †random.fixture(draws).
§regex exposes a small JavaScript-backed regular-expression surface:
λmain()=>String match §regex.compile(
"i",
"^(sigil)-(.*)$"
){
Ok(regex)=>match §regex.find(
"Sigil-lang",
regex
){
Some(found)=>found.full|
None()=>""
}|
Err(_)=>""
}
The canonical regex surface is:
compilefindfindAllisMatch
Regex semantics follow JavaScript RegExp, including pattern syntax and flags. compile validates the pattern/flags first and returns Err on invalid input. find returns the first match; findAll returns all non-overlapping matches as a list. findAll automatically adds the g flag internally — callers do not need to include it.
§json exposes a typed JSON AST with safe parsing:
λmain()=>Unit match §json.parse("{\"ok\":true}"){
Ok(value)=>match §json.asObject(value){
Some(_)=>()|
None()=>()
}|
Err(_)=>()
}
§decode is the canonical layer for turning raw JsonValue into trusted internal Sigil values. For legacy or custom wire formats, define an explicit payload type for the raw JSON shape and translate that payload into the domain type:
t Message={
createdAt:§time.Instant,
text:String
}
t MessagePayload={
createdAt:String,
text:String
}
derive json MessagePayload
λmessage(payload:MessagePayload)=>Result[
Message,
§decode.DecodeError
] match §time.parseIso(payload.createdAt){
Ok(createdAt)=>Ok({
createdAt:createdAt,
text:payload.text
})|
Err(error)=>Err({
message:error.message,
path:["createdAt"]
})
}
The intended split is:
§jsonfor raw parse / inspect / stringify§decodefor decode / validate / trust
If a field may be absent, keep the record exact and use Option[T] in that field. Sigil does not use open or partial records for this.
Derived JSON codecs
For canonical save-state and boundary payloads, Sigil also exposes a compiler-owned derive surface:
t TodoId=TodoId(Int)
t Todo={
done:Bool,
id:TodoId,
text:String
}
derive json Todo
derive json generates same-module helpers for the requested root:
encodeTypeName(value)=>§json.JsonValuedecodeTypeName(value)=>Result[TypeName,§decode.DecodeError]parseTypeName(input)=>Result[TypeName,§decode.DecodeError]stringifyTypeName(value)=>String
For derivable named types, these generated helpers are the only canonical direct JSON codec surface. If an external wire format differs from the domain shape, define an explicit payload or wire type, derive JSON for that payload, and translate between the payload and the domain type with ordinary functions.
Current v1 rules:
- the derive target must be one monomorphic named type
- only explicitly derived roots get public helper names
- nested reachable named types are handled automatically with private helpers
- records encode as exact JSON objects
- lists encode as JSON arrays
{String↦T}encodes as JSON objectsOption[T]encodes asnull | T- ordinary sums encode as
{"tag":"Variant","values":[...]} - wrapper sums of the form
t Name=Name(T)encode as the underlying value - constrained aliases and constrained products validate after decode
Intencodes as JSON numbers and decodes only from integral JSON numbers
To preserve one canonical mapping, v1 rejects:
- generic derive roots
- recursive type graphs
- non-
Stringmap keys - constrained sum types
Option[T]payloads whose canonical encoding can already benull
sigil inspect types reports derived codec metadata under jsonCodecs, including helper names and the resolved wire-format summary. See language/examples/derivedJsonCodecs.sigil for a runnable self-testing example.
§time exposes strict ISO parsing, instant comparison, and harness sleep:
λmain()=>Int match §time.parseIso("2026-03-03"){
Ok(instant)=>§time.toEpochMillis(instant)|
Err(_)=>0
}
Effectful code may also use §time.sleepMs(ms) for retry loops and process orchestration.
§timer exposes event-source timers for long-running app workflows:
λafterMs(ms:Int)=>!Timer Owned[§stream.Source[Unit]]
λeveryMs(ms:Int)=>!Timer Owned[§stream.Source[Unit]]
Timer rules:
afterMsyields one()tick and then finisheseveryMsyields repeated()ticks until the source is closed- both functions return owned stream sources and are intended to be used with
using
§task exposes cancellable background work:
t Task[T]={id:Int}
t TaskResult[T]=Cancelled()|Failed(String)|Succeeded(T)
λcancel[T](task:Task[T])=>!Task Unit
λspawn[T](work:λ()=>T)=>!Task Owned[Task[T]]
λwait[T](task:Task[T])=>!Task TaskResult[T]
Task rules:
spawnreturns an owned task handle and is intended to be used withusingcancelrequests cancellationwaitresolves toSucceeded(value),Cancelled(), orFailed(message)
§terminal exposes a small raw-terminal surface for turn-based interactive programs:
λmain()=>!Terminal Unit={
l _=(§terminal.enableRawMode():Unit);
l key=(§terminal.readKey():§terminal.Key);
l _=(§terminal.disableRawMode():Unit);
match key{
§terminal.Text(text)=>()|
§terminal.Escape()=>()
}
}
The canonical terminal surface is:
clearScreenenableRawModedisableRawModehideCursorshowCursorreadKeywrite
readKey normalizes terminal input into §terminal.Key, currently:
Escape()Text(String)
§url exposes strict parse results and typed URL fields for both absolute and relative targets:
λmain()=>[String] match §url.parse("../language/spec/cli-json.md?view=raw#schema"){
Ok(url)=>[
url.path,
§url.suffix(url)
]|
Err(_)=>[]
}
HTTP Client and Server
§httpClient is the canonical text-based HTTP client layer.
For topology-aware projects, the canonical surface is handle-based rather than raw-URL based:
λmain()=>!Http String match §httpClient.get(
•topology.mailerApi,
§httpClient.emptyHeaders(),
"/health"
){
Ok(response)=>response.body|
Err(error)=>error.message
}
The split is:
- transport/URL failures return
Err(HttpError) - any received HTTP response, including
404and500, returnsOk(HttpResponse) - JSON helpers compose over
§json - topology-aware application code must not pass raw base URLs directly
§topology owns the dependency handles. config/*.lib.sigil now exports world, built through †http, †tcp, and †runtime.
§httpServer is the canonical request/response server layer. For simple programs, serve remains available. For real app/server orchestration, the canonical surface is request-stream based:
t Headers={String↦String}
t HttpBodyError={message:String}
t PendingRequest={request:Request,responder:Responder}
t Request={body:String,headers:Headers,method:String,path:String}
t Responder={id:String}
t Response={body:String,headers:Headers,status:Int}
t RouteMatch={params:{String↦String}}
t Server={port:Int}
t WebSocketClient={id:String}
t WebSocketRoute={handle:§topology.WebSocketHandle,path:String}
λjson(body:String,status:Int)=>Response
λjsonBody(request:Request)=>Result[§json.JsonValue,HttpBodyError]
λlisten(port:Int)=>!Http Owned[Server]
λlistenWithWebSockets(port:Int,routes:[WebSocketRoute])=>!Http Owned[Server]
λlistenWith(handler:λ(Request)=>Response,port:Int)=>!Http Server
λlogRequest(request:Request)=>!Log Unit
λmatch(method:String,pathPattern:String,request:Request)=>Option[RouteMatch]
λnotFound()=>Response
λnotFoundMsg(path:String)=>Response
λok(body:String)=>Response
λport(server:Server)=>Int
λreply(responder:Responder,response:Response)=>!Http Unit
λrequests(server:Server)=>!Http Owned[§stream.Source[PendingRequest]]
λresponse(body:String,contentType:String,status:Int)=>Response
λserve(handler:λ(Request)=>Response,port:Int)=>!Http Unit
λserverError(message:String)=>Response
λwait(server:Server)=>!Http Unit
λwebsocketClose(client:WebSocketClient)=>!Http Unit
λwebsocketConnections(handle:§topology.WebSocketHandle,server:Server)=>!Http Owned[§stream.Source[WebSocketClient]]
λwebsocketMessages(client:WebSocketClient)=>!Http Owned[§stream.Source[String]]
λwebsocketRoute(handle:§topology.WebSocketHandle,path:String)=>WebSocketRoute
λwebsocketSend(client:WebSocketClient,text:String)=>!Http Unit
The public server surface is:
listenlistenWithWebSocketsrequestsreplyjsonBodymatchlistenWithportservewaitwebsocketRoutewebsocketConnectionswebsocketMessageswebsocketSendwebsocketClose
listen returns an owned server handle. requests(server) opens an owned request stream of PendingRequest values, and reply answers one pending request through its Responder.
listenWithWebSockets(port,routes) returns one owned HTTP server handle that also owns exact-path websocket upgrades on the same bound port. Use websocketRoute to declare websocket upgrade paths and websocketConnections(handle,server) / websocketMessages(client) to consume the resulting connection and message streams.
listenWith(handler,port) and serve(handler,port) remain available for simple pure-handler programs. The request-stream surface is the canonical app/server surface for long-running Sigil apps because it composes with using, §task, and §stream.
Passing 0 to listen or serve asks the OS for any free ephemeral port. Use §httpServer.port(server) after listen when the actual port matters.
TCP Client and Server
§tcpClient is the canonical one-request, one-response TCP client layer.
For topology-aware projects, the canonical surface is handle-based:
λmain()=>!Tcp String match §tcpClient.send(
•topology.eventStream,
"ping"
){
Ok(response)=>response.message|
Err(error)=>error.message
}
The canonical framing model is:
- UTF-8 text only
- one newline-delimited request per connection
- one newline-delimited response per connection
§topology owns the dependency handles. config/*.lib.sigil now exports world, built through †http, †tcp, and †runtime.
§tcpServer is the matching minimal TCP server layer:
λhandle(request:§tcpServer.Request)=>§tcpServer.Response=§tcpServer.response(request.message)
λmain()=>!Tcp Unit=§tcpServer.serve(
handle,
45120
)
The public server surface is:
listenportservewait
serve remains the canonical blocking entrypoint for normal programs. listen returns a §tcpServer.Server handle, port reports the actual bound port, and wait blocks on that handle.
Passing 0 to listen or serve asks the OS for any free ephemeral port. Use §tcpServer.port(server) after listen when the actual port matters.
Topology
§topology is the canonical declaration layer for named runtime boundaries. The canonical environment runtime layer now lives under the compiler-owned † roots rather than §config.
§config remains available for low-level binding value helpers inside config modules, but project environments no longer export Bindings. The env ABI is c world=(...:†runtime.World).
Topology-aware projects define src/topology.lib.sigil, src/policies.lib.sigil, the selected config/, and use typed handles instead of raw endpoints or ad hoc sink names in application code:
λmain()=>!Http Unit match §httpClient.get(
•topology.mailerApi,
§httpClient.emptyHeaders(),
"/health"
){
Ok(_)=>()|
Err(_)=>()
}
See [topology.md](/sigil/docs/topology/) for the full model.
List Predicates
Module: stdlib/list
sortedAsc
Check if a list is sorted in ascending order.
λsortedAsc(xs:[Int])=>Bool
Examples:
λmain()=>Bool=§list.sortedAsc([
1,
2,
3
])
and ¤list.sortedAsc([
3,
2,
1
])
and §list.sortedAsc([])
and §list.sortedAsc([5])
Use case: Validate precondition for binary search or other sorted-list algorithms.
sortedDesc
Check if a list is sorted in descending order.
λsortedDesc(xs:[Int])=>Bool
Examples:
λmain()=>Bool=§list.sortedDesc([
3,
2,
1
]) and ¤list.sortedDesc([
1,
2,
3
])
all
Check if all elements in a list satisfy a predicate.
λall[T](pred:λ(T)=>Bool,xs:[T])=>Bool
Examples:
λmain()=>Bool=§list.all(
§numeric.isPositive,
[
1,
2,
3
]
)
and ¤list.all(
§numeric.isPositive,
[
1,
-2,
3
]
)
and §list.all(
§numeric.isEven,
[
2,
4,
6
]
)
Use case: Validate that all elements meet a requirement.
any
Check if any element in a list satisfies a predicate.
λany[T](pred:λ(T)=>Bool,xs:[T])=>Bool
Examples:
λmain()=>Bool=¬§list.any(
§numeric.isEven,
[
1,
3,
5
]
)
and §list.any(
§numeric.isEven,
[
1,
2,
3
]
)
and §list.any(
§numeric.isPrime,
[
4,
6,
8,
7
]
)
Use case: Check if at least one element meets a requirement.
contains
Check if an element exists in a list.
λcontains[T](item:T,xs:[T])=>Bool
Examples:
λmain()=>Bool=§list.contains(
3,
[
1,
2,
3,
4
]
)
and ¤list.contains(
5,
[
1,
2,
3,
4
]
)
and ¤list.contains(
1,
[]
)
Use case: Membership testing.
count
Count occurrences of an element in a list.
λcount[T](item:T,xs:[T])=>Int
countIf
Count elements that satisfy a predicate.
λcountIf[T](pred:λ(T)=>Bool,xs:[T])=>Int
drop
Drop the first n elements.
λdrop[T](n:Int,xs:[T])=>[T]
find
Find the first element that satisfies a predicate.
λfind[T](pred:λ(T)=>Bool,xs:[T])=>Option[T]
Examples:
λmain()=>Bool=(match §list.find(
§numeric.isEven,
[
1,
3,
4,
6
]
){
Some(value)=>value=4|
None()=>false
}) and (match §list.find(
§numeric.isEven,
[
1,
3,
5
]
){
Some(_)=>false|
None()=>true
})
flatMap
Map each element to a list and flatten the results in order.
λflatMap[T,U](fn:λ(T)=>[U],xs:[T])=>[U]
Examples:
λmain()=>Bool=§list.flatMap(
λ(x:Int)=>[Int]=[
x,
x
],
[
1,
2,
3
]
)=[
1,
1,
2,
2,
3,
3
]
inBounds
Check if an index is valid for a list (in range [0, len-1]).
λinBounds[T](idx:Int,xs:[T])=>Bool
Examples:
λmain()=>Bool=§list.inBounds(
0,
[
1,
2,
3
]
)
and §list.inBounds(
2,
[
1,
2,
3
]
)
and ¤list.inBounds(
3,
[
1,
2,
3
]
)
and ¤list.inBounds(
-1,
[
1,
2,
3
]
)
and ¤list.inBounds(
0,
[]
)
Use case: Validate array/list access before indexing. Prevents out-of-bounds errors.
Implementation: Uses #xs to check bounds.
List Utilities
Module: stdlib/list
Note: Use the # operator for list length instead of a function (e.g., #[1,2,3] => 3).
last
Get the last element safely.
λlast[T](xs:[T])=>Option[T]
Examples:
λmain()=>Bool=(match §list.last([]){
Some(_)=>false|
None()=>true
}) and (match §list.last([
1,
2,
3
]){
Some(value)=>value=3|
None()=>false
})
max
Get the maximum element safely.
λmax(xs:[Int])=>Option[Int]
Examples:
λmain()=>Bool=(match §list.max([]){
Some(_)=>false|
None()=>true
}) and (match §list.max([
3,
9,
4
]){
Some(value)=>value=9|
None()=>false
})
min
Get the minimum element safely.
λmin(xs:[Int])=>Option[Int]
Examples:
λmain()=>Bool=(match §list.min([]){
Some(_)=>false|
None()=>true
}) and (match §list.min([
3,
9,
4
]){
Some(value)=>value=3|
None()=>false
})
nth
Get the item at a zero-based index safely.
λnth[T](idx:Int,xs:[T])=>Option[T]
Examples:
λmain()=>Bool=(match §list.nth(
0,
[
7,
8
]
){
Some(value)=>value=7|
None()=>false
}) and (match §list.nth(
2,
[
7,
8
]
){
Some(_)=>false|
None()=>true
})
product
Multiply all integers in a list.
λproduct(xs:[Int])=>Int
Examples:
λmain()=>Bool=§list.product([])=1 and §list.product([
2,
3,
4
])=24
removeFirst
Remove the first occurrence of an element.
λremoveFirst[T](item:T,xs:[T])=>[T]
reverse
Reverse a list.
λreverse[T](xs:[T])=>[T]
sum
Sum all integers in a list.
λsum(xs:[Int])=>Int
Examples:
λmain()=>Bool=§list.sum([])=0 and §list.sum([
1,
2,
3,
4
])=10
take
Take the first n elements.
λtake[T](n:Int,xs:[T])=>[T]
Numeric Helpers
Module: stdlib/numeric
range
Build an ascending integer range, inclusive at both ends.
λrange(start:Int,stop:Int)=>[Int]
Examples:
λmain()=>Bool=§numeric.range(
2,
5
)=[
2,
3,
4,
5
]
and §numeric.range(
3,
3
)=[3]
and §numeric.range(
5,
2
)=[]
Canonical List-Processing Surface
For ordinary list work, Sigil expects the canonical operators and stdlib path, not hand-rolled recursive plumbing:
- use
§list.allfor universal checks - use
§list.anyfor existential checks - use
§list.countIffor predicate counting - use
mapfor projection - use
filterfor filtering - use
§list.findfor first-match search - use
§list.flatMapfor flattening projection - use
reduce ... from ...for reduction - use
§list.reversefor reversal
Sigil now rejects exact recursive clones of all, any, map, filter, find, flatMap, fold, and reverse, rejects #(xs filter pred) in favor of §list.countIf, and rejects recursive result-building of the form self(rest)⧺rhs.
Outside language/stdlib/, Sigil also rejects exact top-level wrappers whose body is already a canonical helper surface such as §list.sum(xs), §numeric.max(a,b), §string.trim(s), xs map fn, xs filter pred, or xs reduce fn from init. Call the canonical helper directly instead of renaming it.
String Operations
Module: stdlib/string
Comprehensive string manipulation functions. These are compiler intrinsics - the compiler emits optimized JavaScript directly instead of calling Sigil functions.
charAt
Get character at index.
λcharAt(idx:Int,s:String)=>String
Examples:
λmain()=>Bool=§string.charAt(
0,
"hello"
)="h" and §string.charAt(
4,
"hello"
)="o"
Codegen: s.charAt(idx)
substring
Get substring from start to end index.
λsubstring(end:Int,s:String,start:Int)=>String
Examples:
λmain()=>Bool=§string.substring(
11,
"hello world",
6
)="world" and §string.substring(
3,
"hello",
0
)="hel"
Codegen: s.substring(start, end)
take
Take first n characters.
λtake(n:Int,s:String)=>String
Examples:
λmain()=>Bool=§string.take(
3,
"hello"
)="hel" and §string.take(
5,
"hi"
)="hi"
Implementation: substring(n, s, 0) (in Sigil)
drop
Drop first n characters.
λdrop(n:Int,s:String)=>String
Examples:
λmain()=>Bool=§string.drop(
2,
"hello"
)="llo" and §string.drop(
5,
"hi"
)=""
Implementation: substring(#s, s, n) (in Sigil, uses # operator)
lines
Split a string on newline characters.
λlines(s:String)=>[String]
Examples:
λmain()=>Bool=§string.lines("a
b
c")=[
"a",
"b",
"c"
] and §string.lines("hello")=["hello"]
Implementation: split(" ", s) (in Sigil)
toUpper
Convert to uppercase.
λtoUpper(s:String)=>String
Examples:
λmain()=>Bool=§string.toUpper("hello")="HELLO"
Codegen: s.toUpperCase()
toLower
Convert to lowercase.
λtoLower(s:String)=>String
Examples:
λmain()=>Bool=§string.toLower("WORLD")="world"
Codegen: s.toLowerCase()
trim
Remove leading and trailing whitespace.
λtrim(s:String)=>String
Examples:
λmain()=>Bool=§string.trim(" hello ")="hello" and §string.trim("
\ttest
")="test"
Codegen: s.trim()
trimStartChars
Remove any leading characters that appear in chars.
λtrimStartChars(chars:String,s:String)=>String
Examples:
λmain()=>Bool=§string.trimStartChars(
"/",
"///docs"
)="docs" and §string.trimStartChars(
"/.",
"../docs"
)="docs"
Codegen: edge trim using the characters listed in chars
trimEndChars
Remove any trailing characters that appear in chars.
λtrimEndChars(chars:String,s:String)=>String
Examples:
λmain()=>Bool=§string.trimEndChars(
"/",
"https://sigil.dev///"
)="https://sigil.dev" and §string.trimEndChars(
"/.",
"docs/..."
)="docs"
Codegen: edge trim using the characters listed in chars
indexOf
Find index of first occurrence (returns -1 if not found).
λindexOf(s:String,search:String)=>Int
Examples:
λmain()=>Bool=§string.indexOf(
"hello world",
"world"
)=6 and §string.indexOf(
"hello",
"xyz"
)=-1
Codegen: s.indexOf(search)
contains
Check whether search appears anywhere within s.
λcontains(s:String,search:String)=>Bool
Examples:
λmain()=>Bool=§string.contains(
"hello world",
"world"
)
and ¤string.contains(
"hello",
"xyz"
)
and §string.contains(
"hello",
""
)
Codegen: s.includes(search)
split
Split string by delimiter.
λsplit(delimiter:String,s:String)=>[String]
Examples:
λmain()=>Bool=§string.split(
",",
"a,b,c"
)=[
"a",
"b",
"c"
] and §string.split(
"
",
"line1
line2"
)=[
"line1",
"line2"
]
Codegen: s.split(delimiter)
replaceAll
Replace all occurrences of pattern with replacement.
λreplaceAll(pattern:String,replacement:String,s:String)=>String
Examples:
λmain()=>Bool=§string.replaceAll(
"hello",
"hi",
"hello hello"
)="hi hi"
Codegen: s.replaceAll(pattern, replacement)
repeat
Repeat a string count times.
λrepeat(count:Int,s:String)=>String
Examples:
λmain()=>Bool=§string.repeat(
3,
"ab"
)="ababab" and §string.repeat(
0,
"ab"
)=""
Implementation: recursive concatenation in Sigil
reverse
Reverse a string.
λreverse(s:String)=>String
Examples:
λmain()=>Bool=§string.reverse("stressed")="desserts" and §string.reverse("abc")="cba"
Codegen: s.split("").reverse().join("")
Current String Surface
§string currently exposes:
charAtcontainsdropendsWithindexOfintToStringisDigitjoinlinesreplaceAllrepeatreversesplitstartsWithsubstringtaketoLowertoUppertrimtrimEndCharstrimStartCharsunlines
Design notes:
- use
#s=0instead of a dedicatedisEmpty - use
§string.trim(s)=""instead of a dedicated whitespace predicate - use
§string.contains(s,search)for containment checks
Float Arithmetic Surface
§float provides IEEE 754 double-precision math via JavaScript's Math object:
abs— absolute valueceil— smallest integer ≥ x (returnsInt)cos— cosine (radians)exp— e^xfloor— largest integer ≤ x (returnsInt)isFinite— true if x is finite (not ±Infinity, not NaN)isNaN— true if x is NaNlog— natural logarithmmax— larger of two floatsmin— smaller of two floatspow— base raised to exponentround— nearest integer, ties round up (returnsInt)sin— sine (radians)sqrt— square roottan— tangent (radians)toFloat— convertInttoFloat(exact)toInt— truncateFloattoward zero (returnsInt)
Functions that can produce NaN or ±Infinity (e.g. sqrt(-1.0), log(0.0)) return those values as valid Float; use isNaN and isFinite to guard at boundaries.
λmain()=>Bool=§float.floor(3.7)=3
and §float.ceil(3.2)=4
and §float.round(2.5)=3
and §float.isNaN(§float.sqrt(-1.0))
Crypto Surface
§crypto provides deterministic hashing and binary-to-text encoding backed by Node.js's node:crypto module and Buffer:
sha256— SHA-256 hash of a UTF-8 string, hex-encodedhmacSha256— HMAC-SHA-256 with the given key, hex-encodedbase64Encode— encode UTF-8 string to base64base64Decode— decode base64 to UTF-8 string (Erron invalid input)hexEncode— encode UTF-8 string to lowercase hexhexDecode— decode hex to UTF-8 string (Erron odd-length or invalid input)
All functions are pure (deterministic, no effect annotation).
λmain()=>Bool match §crypto.base64Decode(§crypto.base64Encode("hello")){
Ok(s)=>s="hello"|
Err(_)=>false
}
Current Numeric Surface
§numeric currently exposes:
absclampdivisibledivmodgcdinRangeisEvenisNegativeisNonNegativeisOddisPositiveisPrimelcmmaxminmodpowrangesign
Examples:
λmain()=>Bool=§numeric.abs(-5)=5
and §numeric.isEven(4)
and §numeric.isPrime(17)
and §numeric.range(
2,
5
)=[
2,
3,
4,
5
]
Core Prelude
ConcurrentOutcome[T,E], Option[T], Result[T,E], Aborted, Failure, Success, Some, None, Ok, and Err are part of the implicit ¶prelude. They do not require qualification.
Current canonical type forms:
t ConcurrentOutcome[T,E]=Aborted()|Failure(E)|Success(T)
t Option[T]=Some(T)|None()
t Result[T,E]=Ok(T)|Err(E)
Typical usage:
λgetOrDefault(default:Int,opt:Option[Int])=>Int match opt{
Some(value)=>value|
None()=>default
}
λprocessResult(res:Result[
String,
String
])=>String match res{
Ok(value)=>"Success: "++value|
Err(msg)=>"Error: "++msg
}
Core Map
¶map is the canonical helper surface for {K↦V} values.
Canonical type and literal forms:
t Headers={String↦String}
c empty=(({↦}:{String↦String}):{String↦String})
c filled=({"content-type"↦"text/plain"}:{String↦String})
Canonical helper surface:
Stability Note
This document describes the current shipped stdlib surface. Placeholder future APIs and older snake_case names are intentionally omitted here. When the surface changes, update the checked declarations and examples in this file instead of keeping speculative or legacy aliases around.