Sigil Standard Library Specification
Version: 1.0.0 Last Updated: 2026-05-02
Overview
The Sigil standard library provides essential types and functions that are automatically available in every Sigil program. The design philosophy emphasizes:
- Minimal but complete - Only include truly universal functionality
- Functional-first - Pure functions, immutability by default
- Type-safe - Leverage strong type system
- Composable - Functions that work well together
- Zero-cost abstractions - Compile to efficient JavaScript
Implicit Prelude and Rooted Modules
The prelude is available in every Sigil module without qualification. Other modules are reached through rooted references such as §list, •topology, †runtime, ※check::log, and ☴router.
Core Types
ConcurrentOutcome[T,E]
Implicit core prelude sum type:
t ConcurrentOutcome[T,E]=Aborted()|Failure(E)|Success(T)
Aborted[T,E]()=>ConcurrentOutcome[T,E]Failure[T,E](error:E)=>ConcurrentOutcome[T,E]Success[T,E](value:T)=>ConcurrentOutcome[T,E]
Option[T]
Represents an optional value - Sigil's null-safe alternative.
t Option[T]=Some(T)|None()
Constructors:
Some[T](value:T)=>Option[T]- Wraps a valueNone[T]()=>Option[T]- Represents absence
Functions:
mapOption(fn,opt)
bindOption(fn,opt)
unwrapOr(fallback,opt)
isSome(opt)
isNone(opt)
Result[T,E]
Represents a computation that may fail - Sigil's exception-free error handling.
t Result[T,E]=Ok(T)|Err(E)
Constructors:
Ok[T,E](value:T)=>Result[T,E]- Success caseErr[T,E](error:E)=>Result[T,E]- Error case
Functions:
mapResult(fn,res)
bindResult(fn,res)
unwrapOrResult(fallback,res)
isOk(res)
isErr(res)
List Operations
Implemented §list Functions
λall[T](pred:λ(T)=>Bool,xs:[T])=>Bool
λany[T](pred:λ(T)=>Bool,xs:[T])=>Bool
λcontains[T](item:T,xs:[T])=>Bool
λcount[T](item:T,xs:[T])=>Int
λcountIf[T](pred:λ(T)=>Bool,xs:[T])=>Int
λdrop[T](n:Int,xs:[T])=>[T]
λfind[T](pred:λ(T)=>Bool,xs:[T])=>Option[T]
λflatMap[T,U](fn:λ(T)=>[U],xs:[T])=>[U]
λinBounds[T](idx:Int,xs:[T])=>Bool
λlast[T](xs:[T])=>Option[T]
λmax(xs:[Int])=>Option[Int]
λmin(xs:[Int])=>Option[Int]
λnth[T](idx:Int,xs:[T])=>Option[T]
λproduct(xs:[Int])=>Int
λremoveFirst[T](item:T,xs:[T])=>[T]
λreverse[T](xs:[T])=>[T]
λsortedAsc(xs:[Int])=>Bool
λsortedDesc(xs:[Int])=>Bool
λsum(xs:[Int])=>Int
λtake[T](n:Int,xs:[T])=>[T]
Safe element access uses Option[T]:
last([])=>None()find(pred,[])=>None()max([])=>None()min([])=>None()nth(-1,xs)=>None()nth(idx,xs)=>None()when out of bounds
Canonical list-processing restrictions
Sigil treats the list-processing surface as canonical:
- 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
The validator 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. These are narrow AST-shape rules, not a general complexity prover.
Outside language/stdlib/, the validator 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. Sigil keeps one canonical helper surface instead of supporting thin local aliases for the same operation.
Implemented §numeric Helpers
t DivMod={quotient:Int,remainder:Int}
λabs(x:Int)=>Int
λclamp(hi:Int,lo:Int,x:Int)=>Int
λdivisible(d:Int,n:Int)=>Bool
λdivmod(a:Int,b:Int)=>DivMod
λgcd(a:Int,b:Int)=>Int
λinRange(max:Int,min:Int,x:Int)=>Bool
λisEven(x:Int)=>Bool
λisNegative(x:Int)=>Bool
λisNonNegative(x:Int)=>Bool
λisOdd(x:Int)=>Bool
λisPositive(x:Int)=>Bool
λisPrime(n:Int)=>Bool
λlcm(a:Int,b:Int)=>Int
λmax(a:Int,b:Int)=>Int
λmin(a:Int,b:Int)=>Int
λmod(a:Int,b:Int)=>Int
λpow(base:Int,exp:Int)=>Int
λrange(start:Int,stop:Int)=>[Int]
λsign(x:Int)=>Int
Implemented §random Functions
λintBetween(max:Int,min:Int)=>!Random Int
λpick[T](items:[T])=>!Random Option[T]
λshuffle[T](items:[T])=>!Random [T]
Semantics:
intBetweenis inclusive and order-insensitive over its two boundspick([])returnsNone()shufflereturns a full permutation of the input list- runtime behavior comes from the active world's
randomentry
Feature Flags
§featureFlags is the canonical stdlib surface for evaluating first-class featureFlag declarations.
Current 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}
Current functions:
λentry[C,T](config:Config[T,C],flag:Flag[T])=>Entry[C]
λget[C,T](context:C,flag:Flag[T],set:Set[C])=>T
Current §featureFlags.get semantics:
- resolve the configured key function, if any
- otherwise evaluate rules in order and stop at the first matching predicate
Value(v)returnsvRollout(r)requires a resolved key and hashes(flag.id,key)
deterministically into the weighted rollout variants, gated by percentage
- if no rule matches, return
flag.default
String Operations
λcharAt(idx:Int,s:String)=>String
λcontains(s:String,search:String)=>Bool
λdrop(n:Int,s:String)=>String
λendsWith(s:String,suffix:String)=>Bool
λindexOf(s:String,search:String)=>Int
λintToString(n:Int)=>String
λisDigit(s:String)=>Bool
λjoin(separator:String,strings:[String])=>String
λlines(s:String)=>[String]
λreplaceAll(pattern:String,replacement:String,s:String)=>String
λrepeat(count:Int,s:String)=>String
λreverse(s:String)=>String
λsplit(delimiter:String,s:String)=>[String]
λstartsWith(prefix:String,s:String)=>Bool
λsubstring(end:Int,s:String,start:Int)=>String
λtake(n:Int,s:String)=>String
λtoLower(s:String)=>String
λtoUpper(s:String)=>String
λtrimEndChars(chars:String,s:String)=>String
λtrimStartChars(chars:String,s:String)=>String
λtrim(s:String)=>String
λunlines(lines:[String])=>String
File and Process Operations
Implemented §file Functions
λappendText(content:String,path:String)=>!Fs Unit
λappendTextAt(content:String,path:String,handle:§topology.FsRoot)=>!Fs Unit
λexists(path:String)=>!Fs Bool
λexistsAt(path:String,handle:§topology.FsRoot)=>!Fs Bool
λlistDir(path:String)=>!Fs [String]
λlistDirAt(path:String,handle:§topology.FsRoot)=>!Fs [String]
λmakeDir(path:String)=>!Fs Unit
λmakeDirAt(path:String,handle:§topology.FsRoot)=>!Fs Unit
λmakeDirs(path:String)=>!Fs Unit
λmakeDirsAt(path:String,handle:§topology.FsRoot)=>!Fs Unit
λmakeTempDir(prefix:String)=>!Fs String
λmakeTempDirAt(prefix:String,handle:§topology.FsRoot)=>!Fs String
λreadText(path:String)=>!Fs String
λreadTextAt(path:String,handle:§topology.FsRoot)=>!Fs String
λremove(path:String)=>!Fs Unit
λremoveAt(path:String,handle:§topology.FsRoot)=>!Fs Unit
λremoveTree(path:String)=>!Fs Unit
λremoveTreeAt(path:String,handle:§topology.FsRoot)=>!Fs Unit
λwriteText(content:String,path:String)=>!Fs Unit
λwriteTextAt(content:String,path:String,handle:§topology.FsRoot)=>!Fs Unit
makeTempDir(prefix) creates a fresh temp directory and returns its absolute path. Cleanup remains explicit through removeTree.
The *At variants are the named-boundary surface for topology-aware projects.
Implemented §cli Types and Functions
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,E,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[E],build:λ(A,B,C,D,E)=>T,description:String)=>RootCommand[T]
λroot6[A,B,C,D,E,F,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[E],arg6:Arg[F],build:λ(A,B,C,D,E,F)=>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,E,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[E],build:λ(A,B,C,D,E)=>T,description:String,name:String)=>Command[T]
λcommand6[A,B,C,D,E,F,T](arg1:Arg[A],arg2:Arg[B],arg3:Arg[C],arg4:Arg[D],arg5:Arg[E],arg6:Arg[F],build:λ(A,B,C,D,E,F)=>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]]
§cli is the canonical typed CLI layer above §process.argv().
CLI rules:
§cli.runowns help and parse-failure output- help exits
0 - parse failure exits
2 - v1 supports one subcommand layer only
- option values stay string-based in v1
§process.argv()remains the only raw argv source
Implemented §process Types and Functions
t Command={argv:[String],cwd:Option[String],env:{String↦String}}
t RunningProcess={pid:Int}
t ProcessResult={code:Int,stderr:String,stdout:String}
t ProcessFailure={code:Int,stderr:String,stdout:String}
λcommand(argv:[String])=>Command
λexit(code:Int)=>!Process Never
λrun(command:Command)=>!Process ProcessResult
λrunAt(command:Command,handle:§topology.ProcessHandle)=>!Process ProcessResult
λrunChecked(command:Command)=>!Process Result[ProcessResult,ProcessFailure]
λrunJson(command:Command)=>!Process Result[§json.JsonValue,ProcessFailure]
λstart(command:Command)=>!Process Owned[RunningProcess]
λstartAt(command:Command,handle:§topology.ProcessHandle)=>!Process Owned[RunningProcess]
λwithCwd(command:Command,cwd:String)=>Command
λwithEnv(command:Command,env:{String↦String})=>Command
λwait(process:RunningProcess)=>!Process ProcessResult
λkill(process:RunningProcess)=>!Process Unit
Process rules:
- command execution is argv-based only
withEnvoverlays explicit variables on top of the inherited environment- non-zero exit codes are reported in
ProcessResult.code runcaptures stdout and stderr in memoryrunCheckedconverts non-zero exit intoErr(ProcessFailure)runJsonrequires zero exit and then parses stdout as JSONstartandstartAtreturn owned process handlesrunAtandstartAtare the named-boundary variants for topology-aware projectskillis a normal termination request, not a timeout/escalation protocolexitterminates the current process and has result typeNever
Implemented §sql Types and Functions
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 topology-backed through
§topology.SqlHandle - SQLite and Postgres are runtime bindings, not separate public stdlib modules
- the portable subset covers one-table full-row
select,insert,update,delete, predicates, ordering, limit, and transactions beginreturns an owned transaction handle- leaving a transaction scope without
commitrolls back §sql.raw...is the only blessed escape hatch for non-portable SQL- raw statements use named parameters written as
:name - raw SQL is non-portable by definition even though parameter binding stays canonical
- joins, projections, aggregates, upsert,
returning, DDL, and vendor-specific operators are outside the v1 portable DSL
Implemented §fsWatch Types and Functions
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 handleswatchAtis the topology-aware named-boundary variant and requires§topology.FsRoot- rename detection is not modeled separately in v1
Implemented §pty Types and Functions
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:
- PTY sessions expose one combined terminal stream rather than split stdout/stderr
eventsyieldsOutput(text)chunks and then oneExit(code)when the session terminates normallywaitresolves to the final exit code for that sessioncloseis a normal session shutdown requestspawnandspawnAtreturn owned session handlesspawnManagedandspawnManagedAtreturn storable runtime-managed session refseventsManagedreturns an owned subscription stream for one managed session refwaitManagedreturns the exit code but leaves the managed ref open untilcloseManagedcloseManagedis idempotentspawnAtis the topology-aware named-boundary variant and requires§topology.PtyHandlespawnManagedAtis the topology-aware managed-ref variant and requires§topology.PtyHandle
Implemented §stream Types and Functions
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 handlespublishfanouts to current subscribers in send order- generic stream failure is not modeled in
§stream; producer APIs own their error events §streamintentionally omits combinator-style operator families in v1
Implemented §websocket Types and Functions
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 scoped to one exact§topology.WebSocketHandlemessagesyields text frames for one clientlisten,connections, andmessagesreturn owned handlessendwrites one text frame to one clientclosecloses one client connection- v1 is server-only and does not expose binary frames, subprotocol negotiation, or a broadcast helper
Implemented §terminal Types and Functions
t Key=Escape()|Text(String)
λclearScreen()=>!Terminal Unit
λdisableRawMode()=>!Terminal Unit
λenableRawMode()=>!Terminal Unit
λhideCursor()=>!Terminal Unit
λreadKey()=>!Terminal Key
λshowCursor()=>!Terminal Unit
λwrite(text:String)=>!Terminal Unit
Terminal rules:
- terminal interaction is raw-key oriented rather than line-oriented
readKeyreturns canonicalKeyvaluesEscape()represents the escape key and escape sequencesText(String)carries normalized plain-text key input- interactive programs should restore cursor visibility and raw-mode state before exit
Implemented §regex Types and Functions
t Regex={flags:String,pattern:String}
t RegexError={message:String}
t RegexMatch={captures:[String],end:Int,full:String,start:Int}
λcompile(flags:String,pattern:String)=>Result[Regex,RegexError]
λfind(input:String,regex:Regex)=>Option[RegexMatch]
λfindAll(input:String,regex:Regex)=>[RegexMatch]
λisMatch(input:String,regex:Regex)=>Bool
Regex rules:
- semantics follow JavaScript
RegExp compilevalidates both flags and pattern before returningOkfindreturns the first match onlyfindAllreturns all non-overlapping matches; adds thegflag internally- unmatched capture groups are returned as empty strings in
captures
Implemented §float Types and Functions
λabs(x:Float)=>Float
λceil(x:Float)=>Int
λcos(x:Float)=>Float
λexp(x:Float)=>Float
λfloor(x:Float)=>Int
λisFinite(x:Float)=>Bool
λisNaN(x:Float)=>Bool
λlog(x:Float)=>Float
λmax(a:Float,b:Float)=>Float
λmin(a:Float,b:Float)=>Float
λpow(base:Float,exp:Float)=>Float
λround(x:Float)=>Int
λsin(x:Float)=>Float
λsqrt(x:Float)=>Float
λtan(x:Float)=>Float
λtoFloat(x:Int)=>Float
λtoInt(x:Float)=>Int
Float rules:
- all functions delegate to
Math.orNumber.in the JS runtime ceil,floor,round,toIntreturnInt(notFloat)toInttruncates toward zero (equivalent toMath.trunc)logis the natural logarithm- functions producing
NaNor±Infinitydo so silently; useisNaN/isFiniteto guard
Implemented §crypto Types and Functions
t CryptoError={message:String}
λbase64Decode(input:String)=>Result[String,CryptoError]
λbase64Encode(input:String)=>String
λhexDecode(input:String)=>Result[String,CryptoError]
λhexEncode(input:String)=>String
λhmacSha256(key:String,message:String)=>String
λsha256(input:String)=>String
Crypto rules:
- all functions are pure (no effect annotation); all inputs are treated as UTF-8
sha256andhmacSha256return lowercase hex stringsbase64DecodeandhexDecodereturnErron invalid input;hexDecodeadditionally errors on odd-length input- backed by
node:crypto(createHash,createHmac) andBuffer
Implemented §time Additions
λsleepMs(ms:Int)=>!Timer Unit
sleepMs is the canonical delay primitive for retry loops and harness orchestration.
Implemented §timer Types and Functions
λafterMs(ms:Int)=>!Timer Owned[§stream.Source[Unit]]
λeveryMs(ms:Int)=>!Timer Owned[§stream.Source[Unit]]
Semantics:
afterMsyields one()tick and then finisheseveryMsyields repeated()ticks until the source is closed- both functions return owned stream sources
Implemented §task Types and Functions
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]
Semantics:
spawnreturns an owned task handlecancelrequests cancellationwaitresolves toSucceeded(value),Cancelled(), orFailed(message)
Map Operations
Maps are a core collection concept, and helper functions live in ¶map.
t Entry[K,V]={key:K,value:V}
λempty[K,V]()=>{K↦V}
λentries[K,V](map:{K↦V})=>[Entry[K,V]]
λfilter[K,V](map:{K↦V},pred:λ(K,V)=>Bool)=>{K↦V}
λfold[K,V,U](fn:λ(U,K,V)=>U,init:U,map:{K↦V})=>U
λfromList[K,V](entries:[Entry[K,V]])=>{K↦V}
λget[K,V](key:K,map:{K↦V})=>Option[V]
λhas[K,V](key:K,map:{K↦V})=>Bool
λinsert[K,V](key:K,map:{K↦V},value:V)=>{K↦V}
λkeys[K,V](map:{K↦V})=>[K]
λmapValues[K,V,U](fn:λ(V)=>U,map:{K↦V})=>{K↦U}
λmerge[K,V](left:{K↦V},right:{K↦V})=>{K↦V}
λremove[K,V](key:K,map:{K↦V})=>{K↦V}
λsingleton[K,V](key:K,value:V)=>{K↦V}
λsize[K,V](map:{K↦V})=>Int
λvalues[K,V](map:{K↦V})=>[V]
JSON Operations
t JsonError={message:String}
t JsonValue=JsonArray([JsonValue])|JsonBool(Bool)|JsonNull|JsonNumber(Float)|JsonObject({String↦JsonValue})|JsonString(String)
λparse(input:String)=>Result[JsonValue,JsonError]
λstringify(value:JsonValue)=>String
λgetField(key:String,obj:{String↦JsonValue})=>Option[JsonValue]
λgetIndex(arr:[JsonValue],idx:Int)=>Option[JsonValue]
λasArray(value:JsonValue)=>Option[[JsonValue]]
λasBool(value:JsonValue)=>Option[Bool]
λasNumber(value:JsonValue)=>Option[Float]
λasObject(value:JsonValue)=>Option[{String↦JsonValue}]
λasString(value:JsonValue)=>Option[String]
λisNull(value:JsonValue)=>Bool
Notes:
parseis exception-safe and returnsErr({message})for invalid JSON.stringifyis canonical JSON output for the providedJsonValue.
Decode Operations
§decode is the canonical boundary layer from raw JsonValue to trusted internal Sigil values.
t DecodeError={message:String,path:[String]}
t Decoder[T]=λ(JsonValue)=>Result[T,DecodeError]
λrun[T](decoder:Decoder[T],value:JsonValue)=>Result[T,DecodeError]
λparse[T](decoder:Decoder[T],input:String)=>Result[T,DecodeError]
λsucceed[T](value:T)=>Decoder[T]
λfail[T](message:String)=>Decoder[T]
λmap[T,U](decoder:Decoder[T],fn:λ(T)=>U)=>Decoder[U]
λbind[T,U](decoder:Decoder[T],fn:λ(T)=>Decoder[U])=>Decoder[U]
λbool(value:JsonValue)=>Result[Bool,DecodeError]
λfloat(value:JsonValue)=>Result[Float,DecodeError]
λint(value:JsonValue)=>Result[Int,DecodeError]
λstring(value:JsonValue)=>Result[String,DecodeError]
λlist[T](decoder:Decoder[T])=>Decoder[[T]]
λdict[T](decoder:Decoder[T])=>Decoder[{String↦T}]
λfield[T](decoder:Decoder[T],key:String)=>Decoder[T]
λoptionalField[T](decoder:Decoder[T],key:String)=>Decoder[Option[T]]
Notes:
§jsonowns raw parsing and inspection.§decodeowns conversion into trusted internal types.DecodeError.pathrecords the nested field/index path of the failure.- If a field may be absent, keep the record exact and use
Option[T]for that field. - Sigil does not use open records or partial records for this boundary story.
Derived JSON codec surface
Sigil also provides a compiler-owned declaration form for canonical JSON codecs:
derive json TypeName
For a derived root TypeName, the compiler exports four same-module helpers:
encodeTypeName(value)=>§json.JsonValuedecodeTypeName(value)=>Result[TypeName,§decode.DecodeError]parseTypeName(input)=>Result[TypeName,§decode.DecodeError]stringifyTypeName(value)=>String
Semantics:
- the derive target must resolve to one monomorphic named type
- for derivable named types, these generated helpers are the only canonical
direct JSON codec surface
- if a legacy or custom wire format differs from the domain shape, user code
should define an explicit payload or wire type, derive JSON for that payload, and translate between payload and domain with ordinary functions
- only explicitly derived roots get public helpers; nested reachable named types use private generated helpers
- records map to exact JSON objects by declared field name
- lists map to JSON arrays
{String↦T}maps to JSON objects and rejects non-Stringkeys at derive timeOption[T]maps tonull | T- ordinary sums map to tagged objects with discriminator fields
tagandvalues - wrapper sums of the form
t Name=Name(T)map to the underlying value - constrained aliases and constrained products decode through the underlying wire shape and then run the constraint as a runtime validation step
Intmaps to JSON numbers and decodes only from integral JSON numbers
Current v1 rejections:
- generic/public roots that are not monomorphic
- recursive type graphs
- constrained sum types
Option[T]whereTcan already encode asnull- unsupported value kinds outside the current JSON mapping surface
sigil inspect types exposes the derived inventory for each analyzed file under jsonCodecs, including target helper names and the normalized wire-format summary.
Time Operations
t Instant={epochMillis:Int}
t TimeError={message:String}
λparseIso(input:String)=>Result[Instant,TimeError]
λformatIso(instant:Instant)=>String
λnow()=>!Clock Instant
λfromEpochMillis(millis:Int)=>Instant
λtoEpochMillis(instant:Instant)=>Int
λcompare(left:Instant,right:Instant)=>Int
λisBefore(left:Instant,right:Instant)=>Bool
λisAfter(left:Instant,right:Instant)=>Bool
Notes:
parseIsois strict ISO-8601 only.- Non-ISO text must be normalized before calling
parseIso.
Math Operations
The numeric helper surface is owned by §numeric; there is no separate math module today.
Logging Operations
λdebug(msg:String)=>!Log Unit
λeprintln(msg:String)=>!Log Unit
λprint(msg:String)=>!Log Unit
λprintln(msg:String)=>!Log Unit
λwarn(msg:String)=>!Log Unit
λwrite(message:String,sink:§topology.LogSink)=>!Log Unit
§log.write is the named-boundary logging surface used by labelled boundary rules.
Module System
Import Syntax
Export Visibility
File extension determines visibility:
.lib.sigil files (libraries):
- All top-level declarations are automatically visible to other modules
- No
exportkeyword needed or allowed
.sigil files (executables):
- Export nothing directly
- Have
main()function
No import declarations, no aliasing, no export lists.
Standard Library Modules
core/prelude
Implicitly available. Contains the foundational vocabulary types:
Option[T]Result[T,E]SomeNoneOkErr
§file
UTF-8 filesystem helpers:
appendTextexistslistDirmakeDirmakeDirsmakeTempDirreadTextremoveremoveTreewriteText
§path
Filesystem path helpers:
basenamedirnameextnamejoinnormalizerelative
§io
Console and process I/O only (print, println, eprintln, warn, debug)
¶map
Dynamic keyed collection helpers over {K↦V} values.
§numeric
Integer helpers (abs, divmod, gcd, lcm, max, min, mod, predicates like isEven, and ranges).
§json
Typed JSON parsing and serialization (JsonValue, parse, stringify)
λparse(input:String)=>Result[JsonValue,JsonError]
λstringify(value:JsonValue)=>String
§decode
Canonical JSON-to-domain decoding (Decoder[T], DecodeError, run, parse)
λrun[T](decoder:Decoder[T],value:JsonValue)=>Result[T,DecodeError]
λparse[T](decoder:Decoder[T],input:String)=>Result[T,DecodeError]
§time
Time and instant handling (Instant, strict ISO parsing, clock access)
λparseIso(input:String)=>Result[Instant,TimeError]
λformatIso(instant:Instant)=>String
λnow()=>!Clock Instant
§topology
Canonical declaration layer for external HTTP and TCP runtime dependencies.
t Environment=Environment(String)
t HttpServiceDependency=HttpServiceDependency(String)
t TcpServiceDependency=TcpServiceDependency(String)
λenvironment(name:String)=>Environment
λhttpService(name:String)=>HttpServiceDependency
λtcpService(name:String)=>TcpServiceDependency
§config
Low-level helper layer for topology-backed environment config data.
Canonical project environment files now export world values built through †runtime, †http, and †tcp. §config remains available inside config modules for binding-shaped helper values, but it is no longer the exported environment ABI.
t BindingValue=EnvVar(String)|Literal(String)
t Bindings={httpBindings:[HttpBinding],tcpBindings:[TcpBinding]}
t HttpBinding={baseUrl:BindingValue,dependencyName:String}
t PortBindingValue=EnvVarPort(String)|LiteralPort(Int)
t TcpBinding={dependencyName:String,host:BindingValue,port:PortBindingValue}
λbindHttp(baseUrl:String,dependency:§topology.HttpServiceDependency)=>HttpBinding
λbindHttpEnv(dependency:§topology.HttpServiceDependency,envVar:String)=>HttpBinding
λbindTcp(dependency:§topology.TcpServiceDependency,host:String,port:Int)=>TcpBinding
λbindTcpEnv(dependency:§topology.TcpServiceDependency,hostEnvVar:String,portEnvVar:String)=>TcpBinding
λbindings(httpBindings:[HttpBinding],tcpBindings:[TcpBinding])=>Bindings
§httpClient
Canonical text-based HTTP client.
t Headers={String↦String}
t HttpError={kind:HttpErrorKind,message:String}
t HttpErrorKind=InvalidJson()|InvalidUrl()|Network()|Timeout()|Topology()
t HttpMethod=Delete()|Get()|Patch()|Post()|Put()
t HttpRequest={body:Option[String],dependency:§topology.HttpServiceDependency,headers:Headers,method:HttpMethod,path:String}
t HttpResponse={body:String,headers:Headers,status:Int,url:String}
λrequest(request:HttpRequest)=>!Http Result[HttpResponse,HttpError]
λget(dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[HttpResponse,HttpError]
λdelete(dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[HttpResponse,HttpError]
λpost(body:String,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[HttpResponse,HttpError]
λput(body:String,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[HttpResponse,HttpError]
λpatch(body:String,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[HttpResponse,HttpError]
λgetJson(dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[JsonValue,HttpError]
λdeleteJson(dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[JsonValue,HttpError]
λpostJson(body:JsonValue,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[JsonValue,HttpError]
λputJson(body:JsonValue,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[JsonValue,HttpError]
λpatchJson(body:JsonValue,dependency:§topology.HttpServiceDependency,headers:Headers,path:String)=>!Http Result[JsonValue,HttpError]
λresponseJson(response:HttpResponse)=>Result[JsonValue,HttpError]
λemptyHeaders()=>Headers
λjsonHeaders()=>Headers
λheader(key:String,value:String)=>Headers
λmergeHeaders(left:Headers,right:Headers)=>Headers
Semantics:
- any successfully received HTTP response returns
Ok(HttpResponse), including404and500 - invalid URL, transport failure, topology resolution failure, and JSON parse failure return
Err(HttpError) - request and response bodies are UTF-8 text in v1
§httpServer
Canonical request/response HTTP server.
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}
λresponse(body:String,contentType:String,status:Int)=>Response
λok(body:String)=>Response
λ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
λmatch(method:String,pathPattern:String,request:Request)=>Option[RouteMatch]
λnotFound()=>Response
λnotFoundMsg(path:String)=>Response
λport(server:Server)=>Int
λreply(responder:Responder,response:Response)=>!Http Unit
λrequests(server:Server)=>!Http Owned[§stream.Source[PendingRequest]]
λserverError(message:String)=>Response
λlogRequest(request:Request)=>!Log Unit
λserve(handler:λ(Request)=>Response,port:Int)=>!Http Unit
λ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
Semantics:
listen(port)returns an owned server handle for request-stream orchestrationlistenWithWebSockets(port,routes)returns one owned HTTP server handle that also owns exact-path websocket upgrades on the same bound portrequests(server)returns an owned stream ofPendingRequestvaluesreplyanswers one pending request through itsResponderlistenWith(handler,port)andserve(handler,port)remain available for simple pure-handler programs- passing
0as the port asks the OS to choose any free ephemeral port port(server)returns the actual bound port, including after a0bindserveandwaitare long-lived once listening succeedswebsocketRoutedeclares one exact websocket upgrade path for one§topology.WebSocketHandlewebsocketConnectionsyields accepted websocket clients for one shared-listener routewebsocketMessagesyields text frames for one websocket clientwebsocketSendandwebsocketCloseact on one websocket client connected through the shared listener
§tcpClient
Canonical one-request, one-response TCP client.
t TcpError={kind:TcpErrorKind,message:String}
t TcpErrorKind=Connection()|InvalidAddress()|Protocol()|Timeout()|Topology()
t TcpRequest={dependency:§topology.TcpServiceDependency,message:String}
t TcpResponse={message:String}
λrequest(request:TcpRequest)=>!Tcp Result[TcpResponse,TcpError]
λsend(dependency:§topology.TcpServiceDependency,message:String)=>!Tcp Result[TcpResponse,TcpError]
Semantics:
- requests are UTF-8 text
- the client writes one newline-delimited message and expects one newline-delimited response
- address validation, socket failure, timeout, topology resolution failure, and framing failure return
Err(TcpError)
§tcpServer
Canonical one-request, one-response TCP server.
t Request={host:String,message:String,port:Int}
t Response={message:String}
t Server={port:Int}
λlisten(handler:λ(Request)=>Response,port:Int)=>!Tcp Server
λport(server:Server)=>Int
λresponse(message:String)=>Response
λserve(handler:λ(Request)=>Response,port:Int)=>!Tcp Unit
λwait(server:Server)=>!Tcp Unit
Semantics:
- the server reads one UTF-8 line per connection
- the handler returns one UTF-8 line response
- the server closes each connection after the response is written
serve(handler,port)is equivalent to blocking on a started serverlistenreturns a server handle that can be observed withportand awaited withwait- passing
0as the port asks the OS to choose any free ephemeral port port(server)returns the actual bound port, including after a0bindserveandwaitare long-lived once listening succeeds
Testing
Testing is built into the language with test declarations and the sigil test runner. There is no current §test module surface.
Implementation Notes
JavaScript Compilation
- Lists compile to JavaScript arrays
- Maps compile to JavaScript Map objects
- Strings are JavaScript strings (UTF-16)
- Integers are JavaScript numbers (beware 32-bit limits!)
- Floats are JavaScript numbers (IEEE 754 double)
Performance Considerations
- List operations are functional (immutable) - use sparingly for large lists
- For performance-critical code, consider using mutable collections explicitly
- String concatenation in loops is O(n²) - prefer §string.join when building from parts
Effect System
Effects are tracked at type level:
!Clock!Fs!FsWatch!Http!Log!Process!Pty!Random!Stream!Tcp!Terminal!Timer!WebSocket- Pure functions have no effect annotation
Projects may define reusable multi-effect aliases in src/effects.lib.sigil.
Future Extensions
Planned for future stdlib versions:
- §concurrency - Threads and channels
See Also
- [Type System](/sigil/spec/type-system/) - Type inference and checking
- [Grammar](grammar.ebnf) - Language syntax
- Implementation: core/prelude.lib.sigil
Next: Implement standard library in stdlib/ directory.