Smallsome 3.0 Language Specification
Version 3.0, draft revision 2. October 10, 2026. One language, two source modes: Smallsome Code and Smallsome Symbols. Project documentation is English. No version 2.1 is introduced.
Reading This Specification
This is the single language specification. It incorporates the complete Cymple 2.0 specification below, not merely a feature summary. Its inherited semantic requirements apply unless explicitly amended by the 3.0 rules here. Historical source syntax, grammar references, version labels and examples in the incorporated source are retained as provenance, not accepted 3.0 syntax.
Precedence is: these 3.0 normative rules, the shared 3.0 grammar
cymple3.ebnf, then inherited normative semantics.
Informative examples never override a normative rule. The target profiles
describe restricted implementations; they do not redefine the full language.
MUST, SHOULD and MAY have the meanings defined in the incorporated source.
The baseline is spec.md at Codeberg commit
c03978c2498db7a78aac4f4ef4ea71550944a1bc, titled Version 2.0, March 3, 2026.
Its Git blob ID is 402b27ccad01eb14b6f3c84f8d30fc3a670b3ac5.
The original grammar.ebnf at the same commit has blob ID
b8708ca316a325d182a885e2979ca23c48384d14 and was compared with the local
keyword draft when preparing the shared grammar.
Source: https://codeberg.org/foodsnacker/Cymple/src/commit/c03978c2498db7a78aac4f4ef4ea71550944a1bc/spec.md
The personal website's resource README still identifies itself as 1.5;
it is not the version authority. The baseline's MIT notice is included below.
Status and Scope
This is a theoretical language specification, not a claim of a complete compiler, standard library or safety-certified runtime. The existing Ozon compiler implements a subset of Code mode. Symbols mode and the additional grammar productions specified here require future front-end work. No compiler implementation is changed by this document. Revision 2 is a consistency audit of both source modes and the theoretical T1 backend contract. It specifies no new executable implementation or lab app.
The complete foundation covers handles, binary64 numbers, nullable values, collections, blocks, ownership, borrowing, functions and tuples, control flow, matching, task lifecycle, race/collect, channels, events, timers, Guru errors, modules, FFI, text, comments, memory model and historical migration examples. The 3.0 amendments below resolve known conflicts rather than hiding them.
Source Modes (Normative)
A compilation unit MUST select exactly one mode: code or symbols.
Mode is compilation metadata, not a source-language directive.
Future tools SHOULD expose --source-mode=code|symbols; this is a proposed
option, not supported by the existing command. Existing tooling defaults
to Code. Projects with several files MAY select a mode per file. Imports
resolve semantic interfaces independently of a module's source mode.
Code mode uses only ASCII syntax and identifiers. Unicode is allowed in strings and comments, so a fully ASCII program is possible without forbidding international text. Symbols mode uses the listed symbol terminals, ASCII identifiers and shared punctuation. Files are UTF-8; identifiers and keywords MUST NOT be rewritten inside strings, comments or foreign names.
No automatic mode guessing and no mixed spellings within a file.
A symbol keyword in Code, or its keyword equivalent in Symbols, is a source
error outside strings/comments. Shared words such as in, mut, every,
detach, of, void, null, built-in names and member names remain
valid in both modes. Symbols is not a different character encoding.
Direction labels send and receive after a channel type's colon, and
the explicit event-emission word progress and machine type word, are
shared in both modes.
They are not alternate Symbols spellings for the send/receive operations.
The target-only phrases on start and on frame are also shared spellings;
they have no independent Symbols alias and are valid only in a profile that
declares those entry points. They are not portable standalone task events.
Both modes normalize to the same token categories, AST, typed IR, ownership checks, scheduler semantics and target backend. A source converter operates on tokens/AST, never global text replacement. Source spans retain original locations for diagnostics. No second parser, interpreter or runtime is required.
Symbols-to-Code Mapping
Each row specifies one semantic terminal in two modes. Context distinguishes declaration, spawn, boolean, event and error uses of shared symbols.
| Symbols 3.0 | Code 3.0 | Meaning |
|---|---|---|
← |
= |
Assignment |
📘 |
const |
Constant declaration, with inferred type |
↩ |
return |
Return |
❓ |
if |
Conditional / pattern guard |
⤵️ |
else |
Alternative |
🔁 i in a..b |
for i in a..b |
Range loop |
🔁 condition |
while condition |
Condition loop |
⏩ n |
step n |
Range step |
🔀 |
match |
Pattern matching |
➜ |
case |
Match arm |
_ |
_ |
Wildcard |
📝 |
# |
Line or indented block comment |
💬 |
print |
Output |
🔽 |
input |
Read one input line, expression |
🔢 |
num |
Number |
word |
word |
Explicit signed 32-bit machine arithmetic, shared spelling |
🔤 |
text |
Text |
✅ (type) |
bool |
Boolean type |
✅ / ✗ (value) |
true / false |
Boolean values |
📋 |
list |
List |
🗺️ |
map |
String-keyed map |
🔣 |
bytes |
Bytearray |
🧱 |
struct |
Struct declaration/type |
💾 |
block |
Memory-block handle |
📡 |
channel |
Directional channel endpoint |
📄 |
file |
File handle |
🌐 |
net |
Network handle |
🧵 name(...) plus block |
fn name(...) plus block |
Function declaration |
🧵 call() without block |
spawn call() |
Task spawn |
🧵 detach call() |
spawn detach call() |
Detached spawn |
🛰️ type |
newchannel type |
Create endpoint pair |
🚀 ch, value |
send ch, value |
Send |
🎯 ch |
receive ch |
Receive |
🌀⚡ |
race |
First successful result |
🌀📦 |
collect |
Gather outcomes |
✅ pattern (clause) |
on success pattern |
Success |
❌ pattern (clause) |
on error pattern |
Failure |
⏩ pattern [every n] |
on progress pattern [every n] |
Progress |
⏹️ pattern |
on stopped pattern |
Cancellation event |
⏱️ duration |
on timeout duration |
Quantum deadline |
⏱️▶ duration, callback |
after duration, callback |
One-shot timer |
⏱️🔁 duration, callback |
every duration, callback |
Periodic timer |
🛑 |
stop |
Cancel enclosing quantum operation |
🔗 x -> [mut] y |
borrow x as [mut] y |
Borrow |
🔌 "library" |
extern "library" |
FFI declarations |
🧘 e |
guru e |
Scope panic handler |
💀 e |
rethrow e |
Rethrow |
❌ Name(message) -> 9000 |
throw Name(message) code 9000 |
Raise named error |
≠ |
!= |
Inequality |
🧩 "module" |
import "module" |
Internal module |
🛠️ "plugin" |
plugin "plugin" |
External plugin |
🎨:name |
colour name |
Optional output colour |
Other punctuation/operators are shared: == < > <= >= + - * / % && || !,
& | ^ << >>, -> for function results, .., brackets, braces, commas
and colons. Shared built-ins include allocate, release, copy,
close, split, filter_ok and filter_error.
Output colour is an optional host service, not part of core computation.
Named colours replace undocumented terminal emoji palettes; Unicode colour
characters in a string are literal data, never hidden commands.
Common Syntax and Values (Normative)
Migration from 2.0
| 2.0 source contract | 3.0 contract in both modes |
|---|---|
| Type prefixes repeated at variable uses | Types only at declarations/signatures |
| Emoji-prefixed interpolation | {expression}, with doubled literal braces |
null_handle |
null |
guru(e) after the Guru symbol |
Single handler identifier |
| Shared move-only channel passed to several tasks | Distinct directional endpoints, explicit split |
| Conflicting queue/blocking admission rules | Bounded FIFO admission without blocking the scheduler |
| Fuzzy simultaneous completion timestamps | Lowest input index at the scheduler decision |
| Conflicting resume-after-panic wording | Handler exits the protected function or rethrows |
| Incomplete syntax productions | One shared grammar for both source modes |
The symbol/keyword choice never changes these rules. Symbols 3.0 is not a compatibility mode for unmodified 2.0 programs; migration must preserve meaning and diagnose ownership ambiguities rather than silently rewriting them.
Layout and Names
Indentation defines blocks. A file chooses tabs or a fixed positive space
width, never mixes them. Blank/comment-only lines do not emit INDENT/DEDENT.
A dedent MUST match an earlier level. Expressions inside brackets may span
lines without changing block indentation. Newlines separate statements.
Identifiers are ASCII letters followed by letters, digits or underscores;
reserved mode keywords cannot be identifiers. Members after a dot may use
names matching type keywords. Longest-match tokenization applies.
To make conversion lossless, the union of both modes' keyword spellings is
reserved for binding/function/module names in BOTH modes. A Code word does
not become a free identifier merely because Symbols is selected. Contextual
direction labels and member names are exceptions only in their stated positions.
The wildcard _ is a pattern, not an identifier. A decimal point must be
followed by a digit; 1..8 is therefore number, range token, number.
For emoji terminals, the listed variation-selector form and its selector-free
presentation map to the same token; arbitrary substitutions and visually
similar symbols are errors. Normalize syntax outside literals to NFC.
Type appears at declaration and in parameter/result signatures only. Repeated type prefixes at uses are removed in both 3.0 modes. Untyped assignment updates an existing binding; it never creates one. Constants infer type from their required initializer, or validate an explicit type annotation. A constant cannot be reassigned or mutated through an alias. Struct fields are declared in an indented block; constructors and destructuring use named fields, not positional field order.
Numbers, Null and Collections
The full language retains 2.0 num: IEEE-754 binary64, not a silently
target-dependent integer. Integer bit operations require exact signed 32-bit
operands; shifts require counts 0..31, right shift is arithmetic, results use
32-bit two's-complement interpretation. Invalid operands raise TypeError.
Number arithmetic uses binary64; remainder is a - trunc(a/b)*b.
Division/remainder by zero raises ArithmeticError (1102).
NaN comparisons follow IEEE-754; no implicit text/number conversion.
A restricted integer target such as Ozon must identify its numeric profile
and reject unsupported constants/operations instead of claiming full conformance.
word is a distinct value type, NOT a target-dependent synonym of num.
It holds signed 32-bit integers. Addition, subtraction, multiplication,
negation and bitwise operations wrap modulo 2^32; comparisons are signed.
Division truncates toward zero; INT_MIN/-1 returns INT_MIN and remainder 0.
Zero division/remainder raises ArithmeticError (1102). Shift counts must be
0..31; right shift is arithmetic. No implicit word/num conversion.
to_num(word) converts exactly; to_word(num) requires a finite exact integer
in signed-32-bit range, otherwise ArithmeticError. Hex word literals denote
32-bit bit patterns when the contextual type is word. Unsuffixed arithmetic
literals otherwise have type num; a literal within word arithmetic is
contextually word only if representable. Mixed nonliteral operands are errors.
No floating-point reduction may be reassociated unless bit-identical results
are proven. Word modulo reductions may be reordered after dependency proof.
Full num operations round once per operator, binary64 round-to-nearest,
ties-to-even; no implicit fused operations. NaN/infinities may arise from
operations; core constants NaN and Infinity are shared built-in value names.
Ordered comparisons with NaN are false, equality is false and inequality true.
Conditions require non-null bool, not numeric truthiness. Logical &&/||
short-circuit left to right. Other operands, call arguments and initializers
evaluate left to right. Equality of scalars/text is by value; move-only owners
compare resource identity without transfer. No implicit deep collection equality.
Comparison/equality chains are forbidden unless explicit parentheses make
each operand's type valid; repeated EBNF forms do not imply Python-style chains.
All positional collections, strings and memory blocks are one-based.
Maps have string keys, not numeric positional indexing.
String indexing/length count Unicode scalar values, not UTF-8 bytes or
grapheme clusters. bytes and untyped block elements are integers 0..255.
Indices and sizes MUST be finite exact integers; sizes are nonnegative.
Map values may be heterogeneous; lists are homogeneous, optionally typed.
Absent map keys raise BoundsError. map of T constrains every value to T;
an unconstrained map is heterogeneous. Nullable types retain null;
null_handle is replaced by null in 3.0. Null/stale dereference panics.
Structs are value aggregates; a field containing a move-only resource makes
the aggregate move-only. Arrays with literal extents are fixed-size extension
types for restricted targets; they retain the same one-based bounds contract.
A read-only owner may inspect/index its resource without transferring it;
mutations require exclusive ownership or a mutable borrow. Passing or assigning
a move-only value transfers ownership. Borrow references cannot escape their
scope, be stored in resources, sent or returned. copy is a deep copy for
collections/blocks and MAY fail allocation; file/network/channel duplication
is not implicit and is not provided by copy.
Reading a move-only element/member produces a non-owning view, not a second
owner. Assigning/passing it to an owning binding moves it out and leaves null
in the source slot. Such extraction requires exclusive ownership. Iteration
and matching bind views; these cannot escape. Scalar/text fields copy by value.
copy rejects any aggregate containing file/network/channel endpoints.
For a list, an empty literal requires its element type from context; otherwise
the first non-null element determines it and all elements must match.
No mutation may invalidate an outstanding element view.
Strings and I/O
Both modes use "{expression}" interpolation. The old emoji-prefixed
interpolation is removed, eliminating ambiguous identifier boundaries.
Literal braces use {{ and }}; escapes are \", \\, \n,
\t, \u{HEX}. Surrogates and out-of-range scalars are errors.
Interpolations evaluate left to right and do not move borrowed operands.
No text + concatenation. print expression writes formatted text plus
a newline. input returns a text line without its line ending, or null at
EOF. Host I/O failure panics. A target without these services must diagnose it.
Functions, Loops and Matching
No classes, generics, closures or nested functions.
Top-level statements run once, then a parameterless main(), if present.
Target events on start/on frame are a separate restricted target profile;
a program cannot combine them with main. They are not portable host events.
In an event-driven program top-level executable initialization is forbidden;
only declarations with constant initializers are allowed. Initialization work
belongs to on start. There is at most one of each target entry point.
Typed functions MUST return the declared value/tuple on every reachable path. Untyped functions return null on fallthrough; void functions return no value. Tuple return arity and declared unpacking types must match. Recursion is valid in the language; unsupported targets reject it. Ordinary calls remain ordinary calls in Symbols; a task prefix is explicit. Variables without an initializer begin as null. A fixed array initializes each element to null unless an explicit initializer supplies its values. Return values are evaluated and moved into a protected result region BEFORE scope cleanup; only that region survives to the caller. Void calls cannot be used as values or quantum operands. A typed return may explicitly be null because types are nullable; implicit fallthrough is still an error.
Range endpoints are inclusive and evaluated once. Default step is +1;
step/symbol step may be any nonzero finite exact integer. Iteration starts
at the stated lower expression (the inherited "10,20,... for 1..100 step 10"
comment is incorrect: the sequence is 1,11,21,...,91).
A positive step with start > end, or negative step with start < end, is empty.
Range bounds/step must be exact finite integers of the same num or word type.
The induction value is immutable inside the body. The next value is computed
mathematically and the loop stops before passing its bound; it never wraps
and repeats on a word overflow. Map iteration binds the key as text. Channel
iteration receives values using the same ownership rules as receive.
No break/continue. Collection iteration preserves list order; maps enumerate
in insertion order. Mutating a collection being iterated is prohibited.
Match arms are tested in order. Literal, range, wildcard, binding and named struct patterns are supported; guards run after bindings are established. Only the first matching arm executes. No match is a no-op for this statement. Bare expression statements evaluate once and discard the result. Pattern bindings are local to the arm. Destructuring examines without moving until a selected arm explicitly transfers an owned field. Guards cannot mutate the inspected value. Named struct fields must exist and may appear only once.
Safety and Runtime Clarifications (Normative)
The incorporated 2.0 normative requirements for generational handles, bounds/alignment checks, RAII, move semantics and share-nothing remain. These amendments take precedence over conflicting inherited prose/examples.
Handle Lifetime
Generation counters MUST NOT wrap and revalidate a stale reference: retire
a slot on counter exhaustion. Validation checks are slot range, occupancy,
generation and kind, in that order. is_valid is the sole non-panicking
validity query; ordinary properties validate first.
Releasing a moved/null/stale handle panics; RAII skips already-released or
moved bindings. On every scope exit, terminate scoped tasks/timers first,
then release resources in reverse acquisition order, exactly once.
Borrow aliases never own or release their backing resource.
Uninitialized allocation is an optional unsafe capability; reading unwritten
bytes MUST panic, not expose previous memory. A warning alone is insufficient.
Tasks and Admission
Running-task cap defaults to 1000 where available and MUST be configurable. A task request is Created, then Queued in FIFO order when capacity is reached, then Running, then Completed/Failed/Cancelled. Admission does not block the scheduler thread. Queued-task memory MUST be bounded by a documented limit; exhaustion raises TaskLimitReached (3000). No unbounded queue is permitted. The inherited blocking-admission prose and unlimited-queue algorithm are superseded. Implementations MUST prevent nested task-cap deadlock by yielding a waiting parent's execution slot, or diagnosing an unschedulable request.
Tasks use isolated mutable heaps. Scalars/immutable text copy by value; move-only arguments transfer to exactly one task. Reusing a moved argument in another task is a compile-time error. Immutable runtime backing may be shared only if observably equivalent to copying and cannot expose mutation. Detach is optional, warned about, and cannot capture a borrow; detached work still counts toward caps and is cancelled at program exit.
Channel Endpoints
newchannel T[:capacity] returns a pair of distinct validated endpoints.
Declare it as channel of T:send tx, channel of T:receive rx = newchannel T.
Symbols substitutes only the terminal spellings. The default capacity is 0
(rendezvous); buffered capacities are nonnegative exact integers.
Each endpoint is move-only and belongs to one task. send requires a send
endpoint and receive a receive endpoint. Moving tx and rx to different
tasks grants no access to each other's heap. This replaces 2.0 examples
passing the same move-only channel into multiple simultaneous tasks.
split(endpoint, count) explicitly consumes one endpoint and returns a list
of count endpoints with the same direction, for fan-out/fan-in; count is a
positive exact integer. No implicit copying. Runtime channel queues carry
ownership transfers, not shared mutable objects. A failed send leaves the
message owned by the sender; a successful send moves it.
close(tx) closes the channel for all senders, rejects future sends with
ChannelClosed, wakes blocked senders and allows buffered data to drain.
After draining, receive returns null. Null messages are forbidden, so EOF is
unambiguous. close(rx) cancels that receiver endpoint; loss of the last
receiver wakes blocked senders with ChannelClosed. Double close of a still
allocated endpoint is a no-op; accessing a released endpoint still panics.
RAII releases endpoints; loss of the last sender closes/drains the channel.
A channel loop ends at closed-and-empty. The runtime alone owns queue
synchronization; mutable language state is never shared through a channel.
Receive on a locally closed endpoint raises ChannelClosed; receive on an open
endpoint whose CHANNEL is closed drains data then returns null. These are
different states. Dropping all receivers destroys undelivered owned messages
exactly once. Dropping a sender does not cancel another live sender. Splitting
must complete atomically or leave the original endpoint owned and unchanged.
Sending a move-only value reserves it while blocked; a failed send restores
the sender's ownership before Guru/unwind. A delivered receive transfers that
message to its receiving task. Endpoint capacity/type/direction never change.
Race, Collect, Deadlines and Cancellation
Quantum operands are a nonempty explicit array of call expressions.
Unspecified create_tasks()/spread-task syntax in historical examples is
not accepted 3.0 syntax. Arguments are evaluated once, in array order.
All operand calls are prepared before any child starts. Preparation failure
destroys reserved argument temporaries and leaves no child running; it does
not resume a moved caller binding. Admission/child failure after launch uses
the normal cancellation/join contract and cannot roll back external effects.
Race waits for the first successful result. Failures do not win. Successful completions carry a model event timestamp. At a scheduler decision, the earliest timestamp wins; only equal timestamps use the lowest one-based input index. Already queued earlier success MUST NOT lose to a later completion merely because both are observed in the same poll. This replaces the fuzzy microsecond tolerance and aligns with T1's integer event ticks. If all tasks fail, invoke the error clause with failures in input order; without that clause raise AggregateFailure (3001). Host wall-clock runs may choose different winners; determinism is guaranteed for identical model events, not for arbitrary external timing or different target clock profiles.
Collect returns input-ordered tagged outcomes Result(value, error, state).
Completed carries the returned value (which may be null) and no error;
Failed carries an Error and no value; Cancelled has neither. The state tag
distinguishes successful null from cancellation. These runtime value objects
are immutable. Both race and collect operands must return the same non-void
type T. Collect binds a list of sealed Result(T) values; the intrinsic payload
type is inferred from the calls, not heterogeneous or a user-defined generic.
If every task fails, run error (or raise AggregateFailure without a handler);
otherwise run success after all complete. External cancellation selects stopped,
not success, even when some earlier tasks completed.
Partial snapshots contain finished outcomes in input order, not completion
order. filter_ok selects successful values; filter_error selects errors.
The outer race binding starts null and receives the winning value. The collect
binding starts empty and owns the current snapshot, finally including terminal
Cancelled entries. Clause pattern bindings are read-only views of their payload;
they cannot escape. Return an outer owned binding, not its clause view.
Copying a snapshot is permitted only when its payload is copyable. A successful
resource payload moves exactly once into the final owner; immutable snapshots
carry borrowed views of it. Loser results are released after cancellation.
There may be at most one clause of each kind. Race rejects progress. Progress runs synchronously after every positive N completions (default 1); snapshots cannot mutate worker state. Deadlines run from quantum entry, including queue delay; units are us/ms/s/m/h, with bare duration in ms. A computed duration is parenthesized and interpreted in ms; negative, nonfinite or fractional base-tick durations are rejected. Units convert exactly to model ticks; a host reports its tick resolution. Deadline comparison uses event timestamps, not when a handler happens to poll. A success at or before the deadline takes precedence over expiry; a later success does not. A timeout cancels remaining work and then invokes timeout; without a handler, raise Timeout (4000). Never run success after timeout/cancellation.
stop sends cancellation immediately, is idempotent and targets only the
innermost lexical quantum operation. It is illegal elsewhere.
It terminates the current clause; statements after stop are unreachable.
Stopped receives the same partial snapshot, if provided. The quantum cannot
return until all scoped children acknowledge cancellation and cleanup.
Thus "immediate stop" means immediate signal, not zero-latency scope exit.
Clause return unwinds the enclosing function only after scoped cleanup.
No child may retain parent resources after the quantum scope ends.
Events and Timers
Standalone task event clauses are declared at the beginning of a function:
success, error, progress and stopped. They register handlers, not statements
that magically emit results. Function completion/panic/cancellation emits the
corresponding event; progress expression explicitly emits progress.
Handlers cannot spawn a continuation into a destroyed frame.
After cleanup, terminal handlers run at most once. Quantum calls consume
their own outcome and must not execute duplicate terminal handlers.
Progress is delivered synchronously to the enclosing event receiver.
These handlers access only their event payload and immutable top-level constants,
not a cleaned-up function's locals/borrowed resources. Normal return data is
protected before cleanup. A terminal handler cannot change that return value,
return from the already completed function, or resume its body. A handler panic
fails the event consumer's scope; it cannot re-enter the completed producer.
Ordinary direct calls execute as direct calls; unconsumed progress is discarded.
Under quantum execution, the quantum clauses are the sole event consumer;
the producer's standalone handlers are not also executed. A standalone spawn
uses its declared handlers. Detached unhandled errors are diagnostics, not
implicit successful completion. Progress every N counts emitted events in
standalone handlers but task completions in collect's progress clause.
after duration, callback and every duration, callback register scope-owned
timers. Callbacks are parameterless named functions; period must be positive.
Timer delivery is serialized in its owner's task, never concurrent access
to its heap. Missed periodic ticks coalesce; callback work does not overlap.
Scope exit cancels timers before resource cleanup. Optional targets lacking
timers/events MUST diagnose the unsupported feature.
Registration requires positive finite durations. Timer count and event queue
are bounded by the target profile; admission failure is TaskLimitReached.
Callbacks cannot create recursive re-entry into an executing owner. No tick
callback may run during unwind or after its timer has been cancelled.
Guru and Errors
Both modes use a leading Guru declaration with one identifier and an
indented handler. Symbols 3.0 removes the redundant guru(e) suffix.
Only one Guru per lexical scope; it catches panics in the following body
and called functions. Inner scopes unwind first. Scoped tasks and resources
are cleaned up before the handler executes; released owners are inaccessible.
The handler receives an immutable Error value with type, code, message,
location, stack_trace and details. It returns from the protected function,
or rethrows into the enclosing scope. Falling through also returns (null for
untyped functions); typed functions must return an appropriate value.
The enclosing function's declared return type applies even to a Guru declared
inside a loop/branch. A cleanup failure records a secondary error without
replacing the original panic, and remaining cleanup still runs.
Execution NEVER resumes the faulting statement or the remainder of its scope.
An error inside the handler propagates outward; it cannot catch itself.
The inherited error tables are unified: retain codes 1000..1003,
1100..1101, 1102 ArithmeticError, 1200 OutOfMemory, 1201 AllocationLimit,
2000 ChannelClosed,
2001 AlreadyClosed (reserved, normal 3.0 close is idempotent),
3000 TaskLimitReached, 3001 AggregateFailure, 4000 Timeout and 4001 Cancelled.
Custom throws use 9000..9999, default 9000; examples using 403/404/custom400
are historical, not valid full-language error codes.
The inherited 1100..1199 category now covers bounds and numeric-domain checks,
including ArithmeticError; its old "Memory & Bounds" label is historical.
Throw requires a non-null text message and a named error type; a core Error
code/name cannot be forged by a custom throw. Error details never expose a
raw pointer, mutable owner or unvalidated foreign memory reference.
Core validation failures cannot be hidden by FFI or optimized checks.
Runtime Error and Result are sealed built-in records, addressed as
struct Error and struct Result where an annotation is required; user
declarations cannot shadow them. Error details are an immutable heterogeneous
map. Result has value/error/state/input_index; state is Completed, Failed or
Cancelled text, and input_index is the original one-based num/word index.
These records are immutable views when received by handlers; taking an owned
payload uses the final quantum binding and normal move rules. The input payload
type T in Result(T) is compiler metadata, not a new generic declaration syntax.
Core built-in names (including these record names) cannot be rebound.
Modules, FFI and Optional Presentation
Imports resolve documented module interfaces and initialize each module once. Cyclic initialization is a compile-time error; declarations may be mutually referenced if initialization is acyclic. Plugins expose the same typed module interface and ownership contract; neither introduces shared mutable globals.
FFI signatures MUST declare C ABI types, resource kind, byte extent,
ownership and matching destructor through target metadata.
No automatic inference of pointer extent is safe. An undeclared pointer
contract MUST be rejected. int* is not a scalar int and MUST NOT use the
inherited "by-value" mapping without an explicit copy contract.
Returning a foreign block wraps a bounded validated handle; its registered
destructor executes exactly once. Passing a borrow to foreign code cannot
retain it. Foreign code is outside the language's safety guarantee and cannot
be advertised as memory-safe merely because its pointer is wrapped.
Optional colour name accepts a documented named palette; unsupported hosts
may ignore presentation but must preserve printed text. Historical benchmark
numbers, binary sizes and CPU-cycle estimates are design commentary, not
measurements or conformance requirements.
Paired 3.0 Examples (Informative)
These illustrate the full language, not an Ozon executable.
Code:
fn sum_to(n: num) -> num
num total = 0
for i in 1..n
total = total + i
return total
num result = sum_to(8)
print "Sum: {result}"
Symbols:
🧵 sum_to(n: 🔢) -> 🔢
🔢 total ← 0
🔁 i in 1..n
total ← total + i
↩ total
🔢 result ← sum_to(8)
💬 "Sum: {result}"
A Code-mode channel pair transfers distinct endpoints to two workers:
fn produce(out: channel of num:send) -> num
send out, 42
close(out)
return 1
fn consume(source: channel of num:receive) -> num
return receive source
channel of num:send tx, channel of num:receive rx = newchannel num
collect list results = [produce(tx), consume(rx)]
on success all
print "{all.length} tasks completed"
The same Symbols source uses 📡 of 🔢:send, 🛰️ 🔢,
🌀📦 📋 results ← ..., ✅ all, 🚀, 🎯 and 🧵.
The endpoint ownership and typed IR are identical.
Compiler Concept and Conformance (Normative)
Core API and Value Access
These contracts apply in both source modes. Names are shared ASCII member or built-in names; host/codec modules use the same typed interface. No implicit host service is available solely because its name appears in an example.
| Operation | Normative value/ownership contract |
|---|---|
| text.length / text[i] | Scalar count / one-scalar text, checked one-based index |
| text.substr(start, count) | Nonnegative integer count, one-based start, checked range; start=length+1 allowed only for count=0 |
| text.upper()/lower() | Unicode default, locale-independent case mapping using the profile's pinned Unicode version; returns new text |
| text.contains(needle) | Exact scalar-sequence membership, bool; no normalization of stored text |
| text.split(separator) | New homogeneous list of text, left-to-right, retains empty fields; empty separator is TypeError |
| list.length / list[i] | Element count / checked value or scoped view |
| list.push(value) | Exclusive mutation; moves a resource value and validates element type |
| list.pop() | Exclusive removal of last item, transferring ownership; null when empty |
| map.length / map[key] | Entry count / text-key lookup, checked missing key |
| bytes.length / bytes[i] | Byte count / checked integer 0..255; mutation requires exclusive access |
| block.length / block[i] | Allocated byte count / checked byte; .size is not a second 3.0 spelling |
| block.is_valid | Bool without a panic, including released/null references; never grants ownership |
| allocate(size) | Owned byte block; size finite nonnegative exact integer, zero-length allowed; zero-initialized bytes |
| release(owner) | Consumes valid owner and makes binding null; resource destructor exactly once |
| copy(value) | Copies scalar/text or deep-copies owned collections/blocks; rejects external-resource fields |
| close(endpoint) | Closes endpoint/channel as specified above, does not release handle storage |
| split(endpoint, count) | Atomic consuming split into bounded same-direction endpoints |
| filter_ok(outcomes) | Consumes an OWNED Result(T) list, moves successful payloads into list of T, destroys remaining owned payloads |
| filter_error(outcomes) | Consumes owned Result(T) list, returns list of immutable Error values, destroys other owned payloads |
| to_num(word) / to_word(num) | Explicit exact conversion / checked narrowing |
Core counts/indices use num by default, or word in a target's explicit bounded machine profile; conversions cannot silently wrap. Unsupported nonconstant text mapping, allocation, views or methods are target rejections. A target must pin Unicode version and allocation/task/queue/stack limits in its manifest. Allocation/size arithmetic is checked before wrap or memory access. There are no source-level typed blocks: block is byte-addressed. Aligned word access belongs to explicit typed library operations with a documented endian/bounds contract, never an implicit pointer cast. Result(T) lists are intrinsic records with payload type inferred at creation; they are not an escape from homogeneous lists. Snapshot views cannot be passed to consuming filter operations. Use the final owning quantum binding.
Host modules may provide file/network services only with explicit capabilities: open/connect returns a unique file/net handle with RAII destructor; read(count) returns owned bytes or null at EOF; write(buffer) borrows the input until synchronous completion and returns a checked byte count. Blocking calls must be cancellation-aware. I/O failures use IOError (5000); foreign ABI failures use ForeignError (8000). Both carry service-specific details. Such services are not implemented or required on T1. Names/paths are module metadata, not a new file transport between codec engines. Colour output is optional presentation.
Scope and Ownership Completeness
Top-level mutable bindings belong to the initial task, not all tasks. Other tasks can see only immutable constants, copied inputs or moved messages. Module functions/types/constants are exported by their declared names; mutable module state is task-local. Imports resolve one canonical module ID, not a second copy per source mode; duplicate names or cycles in initialization are rejected. Qualified module/member calls use the shared call production.
A compile-time borrow/move violation is a source error. A malformed incoming handle or unexpected runtime boundary still panics with the specified Error; debug mode performs all checks. Release checks may be elided only by proof, never across an unknown call/loop or to hide an error. Implementations cannot claim no allocations for text interpolation, deep copy, snapshots or errors: their storage must be accounted for, statically reserved on bounded targets. Typed annotations constrain null-or-T, not arbitrary heterogeneity. Runtime view types are checked by the same ownership analysis as explicit borrows.
Audit and Normative Coverage
| Topic | Single authority after this revision |
|---|---|
| Modes, keyword collisions, symbol contexts | Source Modes, mapping and shared EBNF |
| Numbers, casts, overflow, null, indices | Common Syntax and Values; explicit num/word split |
| Handles, borrowed elements, RAII, return survival | Handle Lifetime and Scope and Ownership Completeness |
| Tasks, queues, endpoints, cancellation | Safety and Runtime Clarifications |
| Race ties, deadline ordering, result ownership | Race, Collect, Deadlines and Cancellation |
| Timers, handlers, cleanup, error records | Events and Timers; Guru and Errors |
| Collections, text, module/FFI services | Core API; Modules, FFI and Optional Presentation |
| T1 lowering and bounded ABI | This document's T1 profile and architecture chapter 28 |
| Old syntax, timings, conflicting 2.0 tables | Frozen provenance only; never an additional 3.0 rule |
The incorporated source's illustrative costs, benchmark claims, old grammar fragments and older normative status banners do not establish 3.0 conformance. Only unchanged semantic requirements are inherited. In particular the full 3.0 clauses above supersede the old channel, panic, task-cap, indexing/property, return, event and numeric wording wherever their contracts overlap. The core error list additionally reserves IOError 5000 and ForeignError 8000. No benchmark, compiler execution, RTL simulation or smoke test is required to claim this document exists; those would be future evidence, not completed work.
Planned Pipeline
Reuse the existing front end and target modules. The planned stages are: mode-aware tokenization -> shared AST -> module/type resolution -> ownership/borrow checking -> task/effect analysis -> typed IR -> target capability validation -> backend -> one runtime contract. Mode spelling cannot reach the backend as an independent semantic choice. Source conversion and pretty-printing are tooling over that same AST.
A complete implementation MUST support both modes and inherited/amended semantics. A restricted target MUST publish numeric, memory, concurrency, I/O and ABI capabilities and state that it is a subset, not full conformance. No silent narrowing, source-level feature dropping or fallback to another parser. Compilation and interpretation must have identical observable semantics under the same target profile.
Future conformance work must cover paired AST/IR equivalence for every terminal, rejected mixed modes, string/comment preservation, prefix migration, range boundaries/steps, tuple arity, pattern guards, UTF-8 indexing, move-after-send, borrow escape, stale generation retirement, cancellation cleanup, nested-cap admission, channel drain, race index ties, Guru unwind, FFI extent/destructor checks and interpreted/compiled agreement. These are obligations for later implementation, not tests run here.
Relationship to Formats and MicroRISC
Smallsome Formats keep their own versioned contracts. Language values do not replace RAU/RFXL/PMF0 bitstreams. Codecs receive explicit bounded byte/block buffers and return typed metadata/results through the same module interface. Symbols and Code call the same semantic codec API; do not duplicate decoders. Parallelism requires disjoint output regions or ownership-transferred packets, plus an explicit join before presenting a frame/audio block. Scheduling cannot change file bytes or codec meaning.
The urisc-t1 compiler/ABI and real-time capability profile are specified in
Smallsome MicroRISC.
They remain theoretical. The 3.0 host binary64 type must not be confused with
T1's integer-only subset. Both source modes lower through the same front end;
unsupported features require target diagnostics. Hard real-time requires
bounded memory, execution and transfer contracts, not merely no GC.
Target urisc-t1 (Theoretical)
The 64-core T1 profile uses word as its native arithmetic type. num is accepted only for operations/ranges whose binary64 behavior can be proven identical under bounded integer lowering, otherwise rejected. In particular num 1/2 is not word 0, and num 2147483647+1 is not word INT_MIN. General binary64 is absent. The compiler must never obtain modulo arithmetic merely by changing target.
T1 has 60 application cores and four system cores. Running tasks may occupy at most the available application cores; bounded queued/waiting descriptors are different from running core occupancy. Language slots and T1's twelve 1-KiB memory slots are unrelated concepts. Paused tasks release execution capacity only at an explicit dispatcher safe point, with a complete lease plan. The static profile rejects detached work, arbitrary host FFI/I/O, unbounded dynamic collections/text, unbounded recursion and unbounded timer admission. Channels, quantum result lists and Error records use statically reserved, bounded resources; this does not enable general-purpose heap allocation. Read-only RAM may be shared physically only through immutable owned regions, not by sending a source-language borrow into another task.
Timed race uses model ticks and input-index tie-breaking. USER messages carry tokens; they are not language endpoints or exceptions by themselves. Transfer acknowledgment establishes visibility, while sender exclusion begins at send reservation. Hardware FAULT stops a core and is never transparently caught by Guru; ordinary checked language errors use the dispatcher error contract. The manifest records language version/revision, source mode per file, numeric types, capabilities, layouts, ownership, quotas, queue limits, cancellation budget, Unicode/model versions and proof assumptions. No T1 compiler/app is implemented or run in this theoretical project stage.
Target Machines and Existing Implementation
Target ozon
This section records the existing executable subset, not full 3.0 conformance. Its integer arithmetic, syscall error numbers and literal-only text are explicit profile limitations. The examples below use that target's local numeric error contract, not the full language's structured Error codes.
Ozon.OS uses a virtual machine with 16 32-bit registers, flat 64-KB task memory, fixed addresses, no heap and no intra-program concurrency. The original interface comprised 69 syscalls; Guru services extend it through syscall 71.
Supported:
num(signed 32-bit) andbool.- Fixed-size arrays, such as
num points[64]. textliterals stored in program data and passed to syscalls; no runtime string construction.- Functions with parameters and returns, up to 32 call levels.
if/else,while,for x in a..b,matchonnum.return, expressions and operators including/and%. Since 2026-10-08 the VM has DIV_RR and MOD_RR (opcodes 34 and 35), signed division truncated toward zero. Division by zero produces VMF_DIVZERO rather than a value; both implementations report the same fault number and address.sys.<name>(...), such assys.plot(x, y, colour)orsys.present().on startandon frame, the task scheduler's two entry points.
Unsupported constructs are diagnosed with the reason:
| Construct | Reason |
|---|---|
Variable-length map/list |
No heap |
struct |
Parsed by the front end; target support remains future work |
block, allocate, release, borrow |
Task memory is flat and already owned by the program |
channel, send, receive, spawn |
An Ozon program is one execution flow |
race, collect, on timeout, stop |
The same execution limitation |
rethrow |
A caught target error is a number; use throw with that number |
extern |
No external libraries on the diskette |
| Runtime text concatenation | No heap or string library |
| Recursion | Function parameters use fixed addresses; no recursive data stack |
The obsolete restriction on nonconstant division/remainder was removed from this table: the existing VM and backend now support DIV_RR/MOD_RR.
The target supports guru and throw through syscalls 70 and 71:
num e = 0
on start
guru e
sys.plot(e, 7, 15) # e is the error code
sys.present()
throw oops("this cannot be done") code 7
guru e installs an error entry point (syscall 71). A later invalid opcode,
explicit throw or excessive CALL depth transfers control there instead
of stopping immediately. The machine stores the error code in e and
removes that entry point. Execution ends after the handler, as with
return; continuing after it would repeat the failed work.
Errors are caught once. An error inside the handler is the terminal Guru state.
The value in e matches VMF_* in vm.asm: 1 unknown opcode,
2 unknown syscall, 3 excessive CALL depth, 4 RET without CALL,
5 a program-raised fault. The text attached to throw is not transported
on this target; only the numeric code arrives.
Other Targets
host (PureBasic on the current machine) and further targets remain planned.
MicroRISC's urisc-t1 backend is specified in the
Smallsome MicroRISC architecture,
not implemented by this naming update. Reuse the existing front end; no
copied grammar or parser. Targets must explicitly report unsupported features.
Incorporated Cymple 2.0 Foundation
The following source is reproduced in full from the fixed commit above. It is the inherited semantic foundation, not a second active specification. Every historical example and syntax fragment in it is subject to the 3.0 amendments and shared grammar above. References to accompanying archive files are historical external references; they do not identify local missing files. The original source's contradictory MUST rules are resolved explicitly above.
Complete frozen Cymple 2.0 source · historical foundation
# CYMPLE: Official Language Specification
**Version 2.0, 2026.03.03**
---
## Overview
Cymple is a procedural programming language using Unicode symbols instead of keywords. It provides validated handles with generational counters, structured concurrency, and explicit safety guarantees for memory and concurrent operations.
Key characteristics:
- **Memory Safe**: Validated handles with generation counters prevent use-after-free
- **Structured Concurrency**: Tasks automatically joined/cancelled on scope exit
- **Bounds Checked**: All memory access validated at runtime
- **No Pointers**: All memory access through validated handles; no pointer types
- **Readable & accessible**: Clear indentation, simple rules, consistent symbols
- **Portable**: UTF-8 (NFC-normalized), tabs for block structure
- **Dual-mode**: Interpreted and compiled with identical semantics
- **Safe**: Exclusive ownership, move-only handles, deterministic cleanup
- **Concurrent**: Preemptive tasks with channels, no shared memory
- **No GC**: RAII ensures immediate resource cleanup without garbage collection pauses
---
## Document Status
**THIS DOCUMENT IS NORMATIVE**
- Sections marked "Normative" define required behavior
- Sections marked "Informative" are examples and guidance
- In case of conflict: Normative sections take precedence
- Version: 2.0 (March 3, 2026)
Key sections:
- ✅ **Normative**: Handles, Memory Blocks, Concurrency, Channels, Error Handling, Formal Semantics
- ℹ️ **Informative**: Code Examples, Design Rationale, Footguns & Guidance
---
## Conformance Keywords (RFC 2119)
This specification uses keywords as defined in RFC 2119 to indicate requirement levels:
- **MUST** / **REQUIRED** / **SHALL**: Absolute requirement for conformance
- **MUST NOT** / **SHALL NOT**: Absolute prohibition
- **SHOULD** / **RECOMMENDED**: Strong recommendation; deviations require good justification
- **SHOULD NOT** / **NOT RECOMMENDED**: Strong discouragement
- **MAY** / **OPTIONAL**: Truly optional; implementation choice
When normative sections conflict, the most specific section takes precedence.
---
## Design Principles (Informative)
Cymple follows these core design principles:
**1. Safety First**
- Memory safety through validated handles and bounds checking
- Concurrency safety through structured concurrency and share-nothing model
- No undefined behavior, fail-fast error detection
**2. Determinism Where Possible**
- Predictable behavior for testing and debugging
- Race operations use lowest-index tie-breaker for simultaneous completion
- RAII cleanup in deterministic reverse-allocation order
- Scope exit behavior is fully specified
**3. Explicitness**
- No hidden allocations or implicit operations
- No background/detached concurrency without explicit syntax
- No shared memory between tasks
- Resource lifetimes visible in code structure
**4. Fail-Fast Philosophy**
- Invalid operations cause immediate errors, not silent corruption
- Handle validation on every use
- Bounds checking on every access
- Channel operations fail clearly on closed channels
**5. Minimalism**
- Small, focused feature set
- Functions and return statements are the primary abstraction
- No break/continue - use functions for control flow
- No garbage collection - RAII provides deterministic cleanup
These principles guide implementation decisions and inform the trade-offs in language design.
---
---
## Cymple 2.0 — Breaking Changes (Informative)
Cymple 2.0 consolidates all breaking changes introduced since v1.4. Code written for v1.5–v1.8 may require the following migrations.
### Removed Syntax
| Was (v1.x) | Now (v2.0) | Reason |
|------------|------------|--------|
| `🔁 i = 1..10` | `🔁 i in 1..10` | One loop syntax only |
| `"a" + 🔤b + "c"` | `"a🔤bc"` | Interpolation covers all cases |
| `!=` | `≠` | EBNF always specified `≠`; `!=` was inconsistent |
| `🔗 "lib.so"` (FFI) | `🔌 "lib.so"` | `🔗` is borrow-only; `🔌` is FFI-only |
| `map.size` | `map.length` | All collections use `.length` |
| `*ptr`, `&addr`, `*🔢` | validated handles | Pointers removed; all memory via handles |
| Memory Block `block[0]` (0-based) | `block[1]` (1-based) | All collections now 1-based |
### Collection Indexing
All indexable collections use **1-based indexing**:
| Type | Indexing |
|------|----------|
| `📋` List | 1-based |
| `🗺️` Map | string keys |
| `🔣` Bytearray | 1-based |
| `💾` Memory Block | 1-based |
| `🔤` String | 1-based |
```cymple
💾buffer ← allocate(100)
buffer[1] ← 42 📝 first slot
buffer[100] ← 99 📝 last slot
buffer[0] ← 5 📝 PANIC: BoundsError (0 < 1)
buffer[101] ← 5 📝 PANIC: BoundsError (101 > 100)
```
### Migration Examples
```cymple
📝 Loops:
🔁 i in 1..100 📝 correct — 🔁 i = 1..100 removed
📝 Strings:
🔤msg ← "Hello, 🔤name!" 📝 correct — + operator removed
📝 Comparison:
❓ x ≠ 0 📝 correct — != removed
📝 FFI:
🔌 "libc.so.6" 📝 correct — 🔗 is now borrow-only
🧵 strlen(s: 🔤) -> 🔢
📝 Memory Block:
💾block ← allocate(100)
block[1] ← 42 📝 correct — 0-based removed
```
Historical details are in `archive/` (per-version changelogs and specs).
---
## Statement Reference Index (Normative)
This index maps EBNF statement forms to their semantic definitions in this specification.
### Declarations & Variables
- `variable_declaration` → **Variables & Assignment**
- `constant_declaration` → **Variables & Assignment**
- `assignment` → **Variables & Assignment**
### Functions & Control Flow
- `function_declaration` → **Functions**
- `return_statement` → **Functions**
- `if_statement` / `else_statement` → **Control Flow**
- `loop_statement` → **Control Flow**
- `match_statement` → **Pattern Matching**
### Memory & Resources
- `allocate_expr` → **Memory Blocks**
- `release_expr` → **Memory Blocks**
- `borrow_statement` → **Borrowing**
### Concurrency & Tasks
- `quantum_race` → **Quantum Operations: Race**
- `quantum_collect` → **Quantum Operations: Collect**
- `stop_signal` → **Quantum Operations: Stop Signal** (valid only within quantum operations)
- `task_spawn` → **Concurrency**
### Channels
- `channel_create` → **Channels**
- `channel_send` → **Channels**
- `channel_receive_statement` / `channel_receive_expr` → **Channels**
- `channel_close` → **Channels**
### Other Statements
- `output_statement` → **I/O**
- `input_statement` → **I/O**
- `guru_meditation` → **Error Handling**
- `struct_declaration` → **Structs**
- `module_import` → **Modules**
- `ffi_declaration` → **FFI**
- `timer_event` → **Timers**
- `color_command` → **Colors** (informative only)
**Note:** For complete syntax, see `grammar.ebnf`. For semantics, see the referenced sections.
---
## Handles (Normative)
**Handles are the core safety mechanism in Cymple.**
### Handle Definition
A handle is an **opaque, validated reference** to a runtime-managed resource. Handles provide memory safety without garbage collection through generational validation and fail-fast error detection.
**Key Properties:**
- **Opaque**: Internal structure not accessible to programmer
- **Typed**: Each handle carries its resource kind
- **Generational**: Contains generation counter for stale detection
- **Validated**: Every use MUST be checked before operation
- **Non-nullable**: Invalid handles cause immediate errors
### Handle Structure (Runtime Internal)
While opaque to the programmer, the runtime MUST maintain this structure:
```
Handle {
slot: u32 // Index into handle table
generation: u32 // Generation counter
kind: ResourceKind // Type of resource (Memory, Channel, File, etc.)
}
```
**IMPORTANT:** This structure is implementation detail. User code MUST NOT access these fields.
### Handle Table
The runtime MUST maintain a global handle table:
```
HandleTable {
slots: Array<HandleSlot>
}
HandleSlot {
occupied: bool // Is this slot in use?
generation: u32 // Current generation counter
kind: ResourceKind // Type of resource
resource: *Resource // Pointer to actual resource
}
```
**Handle Table Operations:**
**Allocate Handle:**
1. Find free slot (occupied = false)
2. If no free slot: grow table or return error
3. Store resource reference
4. Set occupied = true
5. Return Handle { slot, generation, kind }
**Release Handle:**
1. Validate handle (see below)
2. Free/close the resource
3. Increment slot.generation
4. Set occupied = false
5. Slot can be reused with new generation
### Handle Validation (MUST)
**Every handle use MUST perform these checks in order:**
1. **Slot Range Check**
- Is handle.slot < table.slots.length?
- If NO: Panic with "InvalidHandle: slot out of range"
2. **Slot Occupied Check**
- Is table.slots[handle.slot].occupied == true?
- If NO: Panic with "InvalidHandle: slot not occupied (use-after-release)"
3. **Generation Check**
- Is handle.generation == table.slots[handle.slot].generation?
- If NO: Panic with "StaleHandle: generation mismatch (use-after-release)"
4. **Kind Check**
- Is handle.kind == table.slots[handle.slot].kind?
- If NO: Panic with "TypeError: wrong handle type for operation"
**If all checks pass:** Operation proceeds
**If any check fails:** Runtime MUST panic immediately (see Error Handling)
### Handle Types
Cymple defines these handle types:
| Handle Type | Symbol | Resource |
|-------------|--------|----------|
| Memory Block | 💾 | Heap-allocated memory |
| Channel | 📡 | Inter-task communication |
| File | 📄 | File descriptor |
| Network | 🌐 | Network connection |
**Each type MUST have distinct ResourceKind for validation.**
### Handle Lifecycle Example
```cymple
📝 1. Allocation
💾block ← allocate(1024)
📝 Runtime creates handle, allocates resource
📝 2. Use (validated each time)
🔗 block -> B
B[1] ← 42 📝 Validation: ✓ All checks pass
B[100] ← 99 📝 Validation: ✓ All checks pass
📝 3. Release
release(block)
📝 Runtime frees resource, increments generation
📝 4. Stale use (detected!)
📋val ← block[1] 📝 Validation: ✗ Generation mismatch → PANIC
```
### Stale Handle Detection Example
```cymple
🧵 demonstrate_stale_detection()
💾block ← allocate(100)
📝 Use 1: Valid
🔗 block -> B
B[1] ← 42
📝 Validation: slot=5, gen=10, kind=Memory ✓
📝 Release resource
release(block)
📝 Table: slot 5 now has generation=11
📝 Use 2: Stale handle detected!
🧘 guru(e)
🔀 e.type
➜ "StaleHandle"
💬 "Use-after-release detected!"
↩
📋val ← block[1]
📝 Validation fails: handle.gen(10) ≠ slot.gen(11)
📝 Runtime panics with "StaleHandle" error
```
### Optimization Rules
**Compilers MAY optimize validation checks when provably safe:**
1. **Within single borrow scope:**
```cymple
🔗 block -> B
B[1] ← 1 📝 Check required
B[2] ← 2 📝 MAY be elided (same scope)
B[3] ← 3 📝 MAY be elided (same scope)
```
2. **NOT across function boundaries:**
```cymple
process(block) 📝 Full validation MUST occur
```
3. **NOT across loops:**
```cymple
🔁 i in 1..100
block[i] ← i 📝 Full validation MUST occur each iteration
```
**In debug mode:** ALL validations MUST be performed, no elision.
**IMPORTANT:** Safety MUST NEVER be sacrificed for performance.
### Handle vs Pointer Comparison
| Aspect | Cymple Handle | C Pointer |
|--------|---------------|-----------|
| **Safety** | Validated on every use | No validation |
| **Use-after-free** | Impossible (detected) | Undefined behavior |
| **Type safety** | Enforced by kind check | Type casting possible |
| **Null checks** | Not needed (fail-fast) | Manual checks required |
| **Performance** | Small overhead | Zero overhead |
| **Debugging** | Clear error messages | Crashes/corruption |
### Handle Best Practices
**DO:**
- ✅ Use RAII for automatic cleanup
- ✅ Catch validation errors with Guru
- ✅ Release explicitly when needed
- ✅ Trust the validation - it will catch errors
**DON'T:**
- ❌ Try to "optimize away" validation
- ❌ Assume handles are always valid
- ❌ Use handles after release
- ❌ Share handles between tasks (use channels instead)
### Performance Characteristics
**Handle validation cost:**
- Best case: 4 comparisons (~4 CPU cycles)
- Worst case: 4 comparisons + panic (~100+ cycles)
- Amortized: ~0.5-2% overhead in real programs
**Memory overhead:**
- Per handle: 12 bytes (slot + generation + kind)
- Handle table: grows as needed
- Total: ~0.1-1% memory overhead
**IMPORTANT:** This overhead prevents undefined behavior, memory corruption, and security vulnerabilities. It's worth it.
---
## Data Types
### Primitives
**Number (🔢)**
- Unified type for integers and floats
- 64-bit IEEE-754 binary64
- Integers exact up to ±9,007,199,254,740,992
- Range: ≈ ±1.797×10³⁰⁸ ... ±4.94×10⁻³²⁴
- Special values: NaN, +∞, −∞
- Can be `null`
**Bool (✅/✗)**
- ✅ = true
- ✗ = false
- Can be `null`
**String (🔤)**
- Immutable UTF-8 strings
- Escapes: `\"`, `\\`, `\n`, `\t`, `\u{HEX}`
- **String interpolation**: Variables can be embedded directly using their emoji prefix
- Example: `🔤greeting ← "Hello World"`
- Example with interpolation: `💬 "Hello, 🔤name!"`
- Property access: `🔤text.length` returns number of characters
- Single character: `🔤text[1]` (no separate char type)
- Can be `null`
### Collections
**List (📋[...])**
- Mutable, 1-based indexing
- Move-only (requires borrowing for access)
- Homogeneous (single type only)
- Example: `📋nums ← [1, 2, 3]`
- Property access: `📋list.length` returns number of items
- Can be `null`
**Map (🗺️{...})**
- String keys only
- Move-only
- Example: `🗺️data ← {"name": "Alice", "age": 30}`
- Property access: `🗺️map.length` returns number of key-value pairs
- Can be `null`
**Bytearray (🔣[...])**
- Flat binary data container
- Move-only, 1-based indexing
- Example: `🔣bytes ← [0x01, 0x02, 0xFF]`
- Property access: `🔣bytes.length` returns number of bytes
- Can be `null`
### Composite Types
**Struct (🧱)**
```cymple
🧱 Person(name: 🔤, age: 🔢)
p ← Person(name: "Alice", age: 30)
💬 p.name
p.age ← 31
```
Can be `null`
**Handle (💾-based)**
- Resource handles with unique ownership
- Move-only semantics
- RAII: deterministic cleanup at block end (no GC)
- **1-based indexing** (consistent with List, String, Bytearray)
- Prefix indicates resource type: `🎵snd`, `🖼️img`, `💾file`
- Can be `null_handle`
---
## Memory Blocks (Normative)
Memory blocks are heap-managed objects accessed exclusively through validated handles.
### Allocation
**Syntax:**
```cymple
💾block ← allocate(size: 🔢)
```
**Requirements:**
- Runtime MUST allocate `size` bytes on the heap
- Runtime MUST return a valid handle to the memory
- On allocation failure: Runtime MUST panic with "OutOfMemory"
### Indexing
**Syntax:**
```cymple
💾block ← allocate(100)
📋value ← block[index] 📝 Read
block[index] ← value 📝 Write
```
**Validation Requirements (MUST):**
For every index operation `block[i]`:
1. **Handle Validation** (see Handles section)
- Validate slot, generation, and kind
2. **Bounds Check**
- Is i >= 1 AND i <= block.size? (1-based, like all other collections)
- If NO: Panic with "BoundsError: index out of range"
3. **Alignment Check** (for typed blocks)
- Is i properly aligned for the type?
- If NO: Panic with "AlignmentError: unaligned access"
**IMPORTANT:** Bounds checking MUST occur before every access. No exceptions.
### Memory Block Properties
```cymple
💾block ← allocate(1024)
📝 Properties (always safe to access):
🔢size ← block.size 📝 Returns allocated size
✅valid ← block.is_valid 📝 Returns true if handle valid
```
**Property access MUST validate handle first.**
### Copy vs Move Semantics
**Move (default):**
```cymple
💾block1 ← allocate(100)
💾block2 ← block1 📝 block1 is now null_handle
📝 Runtime MUST invalidate block1
📝 Only block2 can access the memory
```
**Copy (explicit):**
```cymple
💾block1 ← allocate(100)
💾block2 ← copy(block1) 📝 Creates NEW allocation
📝 Runtime MUST allocate new memory
📝 Runtime MUST copy contents
📝 Both handles remain valid
```
### Borrowing Memory Blocks
**Read-only borrow:**
```cymple
💾block ← allocate(100)
🔗 block -> B
📋val ← B[1] 📝 OK: Read
B[1] ← 42 📝 ERROR: Cannot mutate read-only borrow
```
**Mutable borrow:**
```cymple
🔗 block -> mut B
B[1] ← 42 📝 OK: Mutable access
📋val ← B[1] 📝 OK: Can also read
```
**Borrow Rules (MUST):**
- Only ONE mutable borrow OR multiple read-only borrows at a time
- Original handle MUST NOT be used while borrowed
- Borrows MUST end before handle is released
### RAII and Automatic Cleanup
```cymple
🧵 process_data()
💾buffer ← allocate(1024)
🔗 buffer -> mut B
B[1] ← 42
B[2] ← 99
📝 Borrow ends here
📝 buffer automatically released here (end of function)
📝 Runtime MUST free memory deterministically
```
**RAII Rules (MUST):**
- Handles MUST be released when scope ends
- Release MUST happen in reverse allocation order
- Release MUST be deterministic (no GC delays)
### Bounds Checking Example
```cymple
🧵 demonstrate_bounds_checking()
💾buffer ← allocate(100)
📝 Catch bounds errors
🧘 guru(e)
🔀 e.type
➜ "BoundsError"
💬 "Index out of range!"
💬 " Index: e.details.index"
💬 " Size: e.details.size"
↩
📝 Safe accesses
buffer[1] ← 1 📝 OK: 1 <= 1 <= 100
buffer[100] ← 99 📝 OK: 1 <= 100 <= 100
📝 Unsafe access (caught!)
buffer[101] ← 5 📝 PANIC: BoundsError (101 > 100)
buffer[0] ← 5 📝 PANIC: BoundsError (0 < 1)
```
### Uninitialized Memory
**Default Behavior:**
- Allocated memory SHOULD be zeroed by default
- Implementation MAY provide `allocate_uninitialized()` for performance
- Uninitialized memory MUST be marked as such
- Reading uninitialized memory SHOULD trigger warning in debug mode
### Memory Block Limits
**Runtime Limits (SHOULD be configurable):**
- Maximum single allocation: Default 2GB
- Maximum total heap: Default system-dependent
- On limit exceeded: Panic with "AllocationLimit"
---
## Null Handling
All types can be `null`. Use the `null` keyword for checks:
```cymple
❓ x == null
💬 "x is not defined"
❓ handle == null_handle
💬 "Invalid handle"
```
---
## Variables & Assignment
**Declaration & Assignment:**
```cymple
🔢x ← 42
🔤name ← "Alice"
```
**Type prefix is mandatory** - this is a core feature that makes variables immediately recognizable and prevents ambiguity.
**Constants (📘):**
```cymple
📘 PI ← 3.1415 📝 Global if at top-level
📘 MAX_SIZE ← 1000
🧵 foo()
📘 LOCAL ← 100 📝 Function-scoped
```
**Handles (move semantics):**
```cymple
💾f1 ← open("data.txt")
💾f2 ← f1 📝 f1 is now null_handle
release(f2) 📝 explicit cleanup (or use RAII)
```
---
## String Operations
**Interpolation (the one way to embed values into strings):**
```cymple
🔤greeting ← "Hello, 🔤name!"
💬 "Count: 🔢count, Name: 🔤name"
```
To combine computed strings, store them in variables first, then interpolate:
```cymple
🔤prefix ← get_prefix()
🔤suffix ← get_suffix()
🔤result ← "🔤prefix🔤suffix"
```
**String methods:**
```cymple
🔢len ← 🔤text.length
🔤upper ← 🔤text.upper()
🔤lower ← 🔤text.lower()
🔤sub ← 🔤text.substr(1, 5)
✅contains ← 🔤text.contains("xyz")
📋parts ← 🔤text.split(",")
```
---
## Collection Operations
**Length/Size access:**
```cymple
📋nums ← [1, 2, 3, 4, 5]
🔢count ← 📋nums.length
🗺️data ← {"a": 1, "b": 2, "c": 3}
🔢size ← 🗺️data.length
🔣bytes ← [0xFF, 0xAA, 0xBB]
🔢len ← 🔣bytes.length
```
---
## Operators
**Assignment:**
- `←` (assignment)
**Comparison:**
- `==` (equal)
- `≠` (not equal)
- `<` `>` `<=` `>=`
**Arithmetic (numbers only; `+` is not valid for strings — use interpolation):**
- `+` `-` `*` `/` `%`
**Logical:**
- `&&` (AND)
- `||` (OR)
- `!` (NOT)
**Bitwise:**
- `&` (AND)
- `|` (OR)
- `^` (XOR)
- `<<` `>>` (shift)
---
## Borrowing (🔗)
Collections are move-only. To access without transferring ownership:
**Read-only borrow:**
```cymple
📋list ← [1, 2, 3]
🔗 list -> L
💬 L[1] 📝 Prints 1
```
**Mutable borrow:**
```cymple
🔗 list -> mut L
L[1] ← 99
L.push(4)
```
**Rules:**
- Borrow is block-scoped
- Cannot store, send via channel, or return borrowed values
- Original value cannot be moved while borrowed
- Ensures exclusive access during borrow
---
## Pattern Matching (🔀)
Match expressions with destructuring and guards:
```cymple
🔀 value
➜ 0
💬 "Zero"
➜ 1..10
💬 "Small"
➜ x ❓ x > 100
💬 "Large: 🔢x"
➜ _
💬 "Default"
```
**With structs:**
```cymple
🔀 person
➜ Person(name: "Alice", age: a)
💬 "Alice is 🔢a years old"
➜ Person(name: n, age: a) ❓ a >= 18
💬 "Adult: 🔤n"
➜ _
💬 "Other person"
```
---
## Control Flow
### If/Else
```cymple
❓ x > 0
💬 "positive"
⤵️
💬 "not positive"
❓ age >= 18
💬 "Adult"
⤵️
❓ age >= 13
💬 "Teenager"
⤵️
💬 "Child"
```
### Loops
**While-style:**
```cymple
🔁 condition
📝 statements
```
**Range loop:**
```cymple
🔁 i in 1..10
💬 "Number: 🔢i"
```
**Collection loop:**
```cymple
🔁 item in list
💬 "Item: 🔤item"
```
**With step:**
```cymple
🔁 i in 1..100 ⏩ 10
💬 "Step: 🔢i" 📝 10, 20, 30, ...
```
---
## Functions
**Declaration:**
```cymple
🧵 add(a: 🔢, b: 🔢) -> 🔢
↩ a + b
🧵 greet(name: 🔤) 📝 No return type for void functions
💬 "Hello, 🔤name!"
```
**Multiple return values:**
```cymple
🧵 divide(a: 🔢, b: 🔢) -> (🔢, ✅)
❓ b == 0
↩ 0, ✗
↩ a / b, ✅
```
**Calling:**
```cymple
🔢result ← add(5, 3)
greet("Alice")
```
**Entry point (main):**
```cymple
🧵 main()
💬 "Program starts here"
🔢result ← calculate()
💬 "Result: 🔢result"
```
If a function named `main()` exists, it is automatically called after all top-level statements are executed. This provides a standard entry point similar to C, Go, Java, and Rust.
### Return Semantics (MUST)
**Typed Functions:**
- Functions with declared return types (`-> 🔢`, `-> 🔤`, etc.) MUST return a value of that type
- Missing or omitted return statement is a compile-time error
- Example: `🧵 foo() -> 🔢` MUST explicitly return a number
**Untyped Functions:**
- Functions without return type declaration implicitly return `null` if no explicit return occurs
- Missing return is not an error; equivalent to implicit `↩ null` at function end
- Example: `🧵 bar()` returns `null` if no `↩` statement is reached
**Void Functions:**
- Functions can optionally declare `-> void` to indicate no return value
- Using `↩ expr` (with a value) in a void function is a compile-time error
- Using `↩` alone (early exit without value) is permitted
- Example: `🧵 log() -> void` can use `↩` for early exit but not `↩ 42`
**Multiple Return Values:**
- Return type `-> (🔢, 🔤)` indicates tuple return
- All return statements in function MUST return matching tuple
- Unpacking: `🔢n, 🔤s ← get_both()`
---
## Concurrency (Normative)
Cymple provides structured concurrency with safety guarantees through validated handles and runtime enforcement.
### Concurrency Model
**Core Principles:**
1. **Share-nothing**: Tasks communicate via channels, not shared memory
2. **Structured**: Tasks are joined/cancelled when leaving scope
3. **Validated**: All concurrency primitives use handle validation
4. **Bounded**: Runtime enforces maximum concurrent tasks
### Runtime Task Cap (MUST)
The runtime MUST enforce a maximum number of concurrent tasks.
**Default Configuration:**
```cymple
📝 Default task cap (implementation-defined, recommended: 1000)
runtime.max_concurrent_tasks ← 1000
```
**Enforcement Behavior:**
When task cap is reached, the runtime MUST:
1. Block the task creation request
2. Wait for a task slot to become available
3. Resume once a task completes
The runtime MUST NOT:
- Silently drop task creation requests
- Create tasks beyond the cap
- Ignore the cap setting
**Rationale:** Prevents resource exhaustion and "spawn storms".
### Structured Concurrency (MUST)
Tasks started in a scope MUST be joined or cancelled when leaving that scope.
**Automatic Cleanup:**
```cymple
🧵 parent_function()
🌀📦 📋results ← [task1(), task2(), task3()]
⏱️ 30s
💬 "Timeout"
✅ 📋all
↩ 📋all
📝 GUARANTEE: All tasks are done or cancelled here
💬 "Scope exit: all tasks finished"
```
**Scope Exit Rules (MUST):**
- Runtime MUST wait for all tasks to complete OR
- Runtime MUST cancel all running tasks
- No tasks may outlive their parent scope
- Exception: Explicitly detached tasks (see below)
### Task Lifecycle States
```
Created → Queued → Running → [Completed | Cancelled | Failed]
```
**State Transitions (MUST):**
- Created: Task object allocated
- Queued: Waiting for task slot (if cap reached)
- Running: Actively executing
- Completed: Finished successfully
- Cancelled: Stopped by parent scope exit
- Failed: Panicked with error
### Explicit Detach (MAY)
Tasks MAY be explicitly detached from their parent scope:
```cymple
🧵 fire_and_forget()
🧵detach background_task()
📝 WARNING: Task outlives this function!
↩ 📝 Parent returns immediately
```
**Detach Rules:**
- Detached tasks MUST NOT prevent program termination
- Detached tasks SHOULD complete before main() exits
- Implementation SHOULD warn about detached tasks
- Detached tasks count toward task cap
**Use Detach Sparingly:** Prefer structured concurrency.
### Quantum Operations
#### Race (`🌀⚡`) - First Result Wins
**Syntax:**
```cymple
🌀⚡🔤result ← [task1(), task2(), task3()]
⏱️ timeout
📝 timeout handler
✅ 🔤winner
📝 success handler
❌ 🔤error
📝 error handler
```
**Semantics (MUST):**
1. Start all tasks concurrently
2. Wait for first successful result
3. Cancel all other tasks immediately
4. Return the winning result
**Timeout (SHOULD):**
- Race operations SHOULD have explicit timeout
- Without timeout: may block indefinitely
- Timeout fires if no task completes in time
#### Collect (`🌀📦`) - Gather All Results
**Syntax:**
```cymple
🌀📦 📋results ← [task1(), task2(), ...taskN()]
⏱️ timeout
📝 timeout handler (returns partial)
⏩ 📋partial every N
📝 progress handler
❓ condition
🛑 📝 early cancellation
✅ 📋all
📝 success handler
❌ 🔤total_failure
📝 all-failed handler
```
**Semantics (MUST):**
1. Start all tasks concurrently (subject to cap)
2. Wait for all tasks to complete
3. Return list of results (including failures)
4. Support partial results on timeout
**Progress Events:**
- `every N`: Fire progress after every N completed tasks
- Without `every`: Fire after EACH task (expensive!)
- Runtime SHOULD rate-limit progress events
**Early Cancellation:**
- `🛑` stops all remaining tasks
- Returns partial results immediately
- Cancelled tasks MUST cleanup resources
### Stop Signal (`🛑`)
The stop signal MUST:
- Propagate to all running tasks in the quantum operation
- Cause blocked operations to unblock
- Be non-blocking itself
- Work across all quantum operations
```cymple
⏩ 📋partial every 10
❓ user_cancelled()
🛑 📝 Stops all tasks immediately
↩ 📋partial
```
### Task Communication
Tasks MUST communicate via channels (see Channels section).
**Forbidden:**
- Shared memory between tasks
- Direct task-to-task pointers
- Global mutable state
**Allowed:**
- Channels for message passing
- Read-only data sharing (via immutable references)
### Concurrency Safety Examples
**Example 1: Structured Timeout**
```cymple
🧵 fetch_with_timeout(🔤url) -> 🔤
🌀⚡ 🔤result ← [http_get(🔤url)]
⏱️ 5s
💬 "Request timed out"
↩ ""
✅ 🔤data
↩ 🔤data
📝 Task guaranteed cleaned up here
```
**Example 2: Batch Processing with Cap**
```cymple
🧵 process_batch(📋items) -> 📋
📝 Runtime enforces cap (e.g., 1000 tasks max)
📝 Excess tasks wait in queue
🌀📦 📋results ← create_tasks(📋items)
⏱️ 5m
↩ 📋results 📝 Partial results
⏩ 📋partial every 100
💬 "Progress: 📋partial.length"
✅ 📋all
↩ 📋all
```
**Example 3: Early Cancellation**
```cymple
🧵 search_until_found(📋databases) -> 🔤
🌀📦 📋results ← search_all(📋databases)
⏩ 📋partial
❓ found_match(📋partial)
💬 "Found! Stopping search"
🛑 📝 Cancel remaining tasks
↩ extract_match(📋partial)
✅ 📋all
↩ best_result(📋all)
```
### Performance Considerations
**Task Creation Cost:**
- Lightweight: ~1-10 microseconds per task
- Subject to cap enforcement delays
**Context Switch Cost:**
- Preemptive scheduling overhead
- ~1-5 microseconds per switch
**Validation Overhead:**
- Handle validation on channel operations
- ~4 comparisons per operation
**IMPORTANT:** These costs are small compared to the safety guarantees provided.
---
## Channels (Normative)
Channels are type-safe, validated communication primitives for inter-task communication.
### Channel Properties
**Channels MUST:**
- Be referenced via validated handles
- Support ONLY one-way communication (send OR receive)
- Be type-safe (enforce message type)
- Close deterministically
- Not allow shared memory passing
### Channel Creation
```cymple
📡ch ← 🛰️🔢 📝 Create channel for numbers
```
**Requirements:**
- Runtime MUST allocate channel with specified type
- Runtime MUST return valid channel handle
- Channel MUST be initially open
### Channel Operations
**Send:**
```cymple
🚀 ch, value
```
**Send Requirements (MUST):**
1. Validate channel handle
2. Check channel is open
3. Type-check value matches channel type
4. Block if channel full (implementation-defined buffer)
5. On closed channel: Panic with "ChannelClosed"
**Receive:**
```cymple
🔢value ← 🎯 ch
```
**Receive Requirements (MUST):**
1. Validate channel handle
2. Check channel is open
3. Block until message available
4. Return typed value
5. On closed empty channel: Return special "closed" value or panic
**Loop over Channel:**
```cymple
🔁 msg in ch
💬 "Received: 🔢msg"
📝 Loop exits when channel closed and empty
```
### Channel Close Semantics (MUST)
**Explicit Close:**
```cymple
📡ch ← 🛰️🔢
🚀 ch, 42
close(ch) 📝 Channel MUST close here
```
**RAII Close:**
```cymple
🧵 sender()
📡ch ← 🛰️🔢
🚀 ch, 42
📝 Channel MUST close at scope exit
```
**Close Requirements (MUST):**
- Channel MUST be closeable exactly once
- After close: new sends MUST panic
- After close: pending receives complete
- After close + empty: receives return "closed"
- Close MUST be visible to all receivers
### Closed Channel Behavior
| Operation | Behavior | Required |
|-----------|----------|----------|
| Send to closed | Panic "ChannelClosed" | MUST |
| Receive from closed (empty) | Return null/closed | MUST |
| Receive from closed (has data) | Return data | MUST |
| Loop over closed | Exit loop | MUST |
| Double close | Panic "AlreadyClosed" | SHOULD |
### Channel Example with Guru
```cymple
🧵 safe_channel_use()
🧘 guru(e)
🔀 e.type
➜ "ChannelClosed"
💬 "Channel was closed"
↩
📡ch ← 🛰️🔢
🌀📦 _ ← [
producer(ch),
consumer(ch)
]
✅ _
💬 "Done"
🧵 producer(📡out)
🔁 i in 1..10
🚀 out, i
close(out) 📝 Signal end
🧵 consumer(📡in)
🔁 msg in in
💬 "Got: 🔢msg"
💬 "Consumer finished"
```
### Channel Capacity and Buffering
**Default Behavior:**
- Channels MAY be unbuffered (synchronous)
- Channels MAY be buffered (asynchronous)
- Implementation MUST document default
**Buffered Channel:**
```cymple
📡ch ← 🛰️🔢:100 📝 Buffer size 100
```
**Buffering Rules:**
- Send blocks when buffer full
- Receive blocks when buffer empty
- Close releases blocked senders/receivers
### Channel Safety Rules (MUST NOT)
**Forbidden Operations:**
- ❌ Sharing channels across tasks via shared memory
- ❌ Casting channel to different type
- ❌ Accessing channel internals
- ❌ Using channel after release
**Allowed Operations:**
- ✅ Passing channel via function parameters
- ✅ Returning channel from functions
- ✅ Storing channels in collections (moves)
- ✅ Borrowing channels temporarily
### Channel Patterns
**Pattern 1: Producer-Consumer**
```cymple
🧵 main()
📡ch ← 🛰️🔤
🌀📦 _ ← [
produce(ch),
consume(ch)
]
✅ _
💬 "Pipeline complete"
🧵 produce(📡out)
🔁 i in 1..100
🚀 out, "Item i"
close(out)
🧵 consume(📡in)
🔁 item in in
process(item)
```
**Pattern 2: Fan-out**
```cymple
🧵 fan_out(📡in, 📋workers: 📡)
🔁 data in in
📝 Distribute to workers
🔁 worker in workers
🚀 worker, data
📝 Close all workers
🔁 worker in workers
close(worker)
```
**Pattern 3: Select (via Race)**
```cymple
🧵 select_channel(📡ch1, 📡ch2)
🌀⚡ 🔢result ← [
receive_task(ch1),
receive_task(ch2)
]
✅ 🔢value
💬 "Got: 🔢value"
↩ 🔢value
```
---
## Events
Tasks can emit events with dedicated event blocks:
```cymple
🧵 download(url: 🔤)
✅ data
💬 "Downloaded 🔢data.length bytes"
❌ error
💬 "Error: 🔤error"
⏩ progress
💬 "Progress: 🔢progress%"
```
**Event types:**
- `✅` Success
- `❌` Error
- `⏩` Progress
- `⏹️` Stopped
---
## Timers
**One-shot timer:**
```cymple
⏱️▶ 1000, my_callback 📝 Calls after 1000ms
```
**Periodic timer:**
```cymple
⏱️🔁 1000, my_callback 📝 Calls every 1000ms
```
---
## Error Handling (Normative) and Panic Behavior
### Panic Mechanism
When validation or safety checks fail, Cymple uses a **panic mechanism** similar to Rust and Go.
**Panic Behavior (MUST):**
When a panic occurs, the runtime MUST:
1. Stop the current operation immediately
2. Unwind the call stack (in order)
3. Execute RAII cleanup for each scope
4. Release all handles in reverse allocation order
5. Propagate to Guru handler if present
6. Otherwise: Terminate the task/program
**Panic is NOT:**
- Resumable (no "continue after panic")
- Catchable except via Guru
- For recoverable errors (use Result types instead)
### Guru Meditation (`🧘`)
Guru is Cymple's mechanism for catching and handling panics.
**Syntax:**
```cymple
🧘 guru(e)
📝 Pattern match on error
🔀 e.type
➜ "ErrorType1"
📝 Handle this error
➜ "ErrorType2"
📝 Handle another error
➜ _
💀 e 📝 Rethrow
```
**Guru Scope:**
- Guru MUST be declared at beginning of function or block
- Guru covers all code in that scope
- Inner Gurus override outer Gurus
- Guru catches panics from called functions
**Guru Requirements (MUST):**
- Runtime MUST call Guru on any panic in scope
- Guru MUST receive error object with full context
- Guru MAY rethrow with `💀 e`
- Guru MAY return value to continue execution
### Standard Error Types
The runtime MUST provide these error types:
| Error Type | Code | When |
|------------|------|------|
| `HandleValidation` | 1000 | Handle validation failed |
| `StaleHandle` | 1001 | Generation mismatch |
| `InvalidHandle` | 1002 | Slot invalid/unoccupied |
| `TypeError` | 1003 | Handle kind mismatch |
| `BoundsError` | 1100 | Index out of range |
| `AlignmentError` | 1101 | Unaligned memory access |
| `OutOfMemory` | 1200 | Allocation failed |
| `AllocationLimit` | 1201 | Exceeded allocation limit |
| `ChannelClosed` | 2000 | Operation on closed channel |
| `TaskLimitReached` | 3000 | Too many concurrent tasks |
| `Timeout` | 4000 | Operation timed out |
| `Cancelled` | 4001 | Task was cancelled |
### Error Object Structure
```cymple
Error {
type: 🔤 📝 Error type string
code: 🔢 📝 Numeric error code
message: 🔤 📝 Human-readable message
location: 🔤 📝 File:Line information
details: 🗺️ 📝 Additional context
}
```
**Details Map:**
- `BoundsError`: { index: 🔢, size: 🔢 }
- `StaleHandle`: { slot: 🔢, expected_gen: 🔢, actual_gen: 🔢 }
- `TypeError`: { expected: 🔤, actual: 🔤 }
### Validation Error Examples
**Example 1: Handle Validation**
```cymple
🧵 safe_access(💾block, 🔢index) -> 📋
🧘 guru(e)
🔀 e.type
➜ "StaleHandle"
💬 "Error: Use-after-release detected"
💬 " Slot: e.details.slot"
💬 " Expected gen: e.details.expected_gen"
💬 " Actual gen: e.details.actual_gen"
↩ null
➜ "BoundsError"
💬 "Error: Index out of bounds"
💬 " Index: e.details.index"
💬 " Size: e.details.size"
↩ null
➜ _
💀 e 📝 Rethrow unknown errors
↩ block[index]
```
**Example 2: Task Limit**
```cymple
🧵 spawn_carefully()
🧘 guru(e)
🔀 e.type
➜ "TaskLimitReached"
💬 "Too many tasks, waiting..."
📝 Retry after delay
↩
🌀📦 📋results ← [many_tasks...]
✅ 📋all
↩ 📋all
```
### Debug vs Release Mode
**Debug Mode MUST:**
- Perform ALL validation checks
- Include full error context
- Never elide bounds checks
- Provide verbose error messages
- Include source location information
**Release Mode MAY:**
- Optimize validation where provably safe
- Provide shorter error messages
- Elide some debug information
- MUST NEVER sacrifice safety
**Configuration:**
```cymple
📝 Compiler flags (example)
cymple --mode=debug 📝 All checks, verbose errors
cymple --mode=release 📝 Optimized, but still safe
```
### Stack Unwinding
On panic, the runtime MUST unwind the stack:
**Unwinding Process:**
1. Start at panic location
2. For each stack frame (from inner to outer):
- Execute RAII cleanup for that scope
- Release handles in reverse order
- Check for Guru handler
3. If Guru found: call Guru with error
4. If Guru handles: resume execution
5. If Guru rethrows or not found: continue unwinding
6. If unwinding reaches top: terminate task/program
**Example Unwinding:**
```cymple
🧵 outer()
🧘 guru(e) 📝 Catches inner panics
💬 "Caught: e.type"
inner()
🧵 inner()
💾block ← allocate(100)
block[200] ← 5 📝 PANIC: BoundsError
📝 Stack unwinds:
📝 1. Release 'block'
📝 2. Exit inner()
📝 3. Find Guru in outer()
📝 4. Call Guru with error
```
### Fail-Fast Philosophy
Cymple follows a **fail-fast** approach:
**Principles:**
- Detect errors as early as possible
- Never continue with invalid state
- Prefer explicit panic over silent corruption
- Make bugs obvious during development
**Benefits:**
- Easier debugging (errors caught early)
- No undefined behavior
- No memory corruption
- Clear error messages
**Cost:**
- Small runtime overhead for checks
- Must handle panics with Guru
**IMPORTANT:** Fail-fast prevents security vulnerabilities and data corruption.
### Error Propagation
**Manual Propagation:**
```cymple
🧵 caller()
🧘 guru(e)
💀 e 📝 Rethrow to caller's caller
callee() 📝 May panic
🧵 callee()
risky_operation() 📝 May panic
```
**Automatic Propagation:**
- If no Guru present: panic propagates up automatically
- Unwinding continues until Guru found or program exits
### Custom Error Throwing
```cymple
🧵 validate_input(🔤input)
❓ input.length == 0
❌ ValidationError("Input cannot be empty")
❓ input.length > 1000
❌ ValidationError("Input too long") -> 400
```
**Throw Syntax:**
- `❌ ErrorType(message)` - Throw with message
- `❌ ErrorType(message) -> code` - Throw with custom code
---
## Formal Semantics (Normative)
**THIS SECTION IS NORMATIVE** - All implementations MUST conform to these semantics.
### Task Lifecycle State Machine
**States:**
```
Created → Queued → Running → [Completed | Cancelled | Failed]
↓ ↗
TaskCap Timeout/Stop
```
**State Definitions:**
1. **Created**: Task object allocated, not yet started
- MUST have unique task ID
- MUST be registered in task table
- Parent reference MUST be stored
2. **Queued**: Waiting for task slot (when at cap)
- MUST be in FIFO queue
- MUST not consume task slot yet
- Cancellation MUST remove from queue
3. **Running**: Actively executing
- MUST consume one task slot
- MUST be preemptible
- MUST respond to stop signals
4. **Completed**: Finished successfully
- MUST release task slot
- Result MUST be available
- MUST trigger parent notifications
5. **Cancelled**: Stopped before completion
- MUST release task slot
- MUST execute cleanup (RAII)
- MUST NOT be resumable
6. **Failed**: Panicked with error
- MUST release task slot
- Error MUST propagate to parent
- MUST execute unwinding
**Transition Rules (MUST):**
```
Created → Queued: When task cap reached
Queued → Running: When task slot available
Running → Completed: Normal return
Running → Cancelled: Stop signal OR parent scope exit
Running → Failed: Panic occurs
Queued → Cancelled: Parent scope exit before slot available
```
---
### Scope Exit Semantics (Structured Concurrency)
**DEFINITION:** A scope exits when execution reaches its closing brace (DEDENT in EBNF).
**Scope Exit MUST perform these steps IN ORDER:**
1. **Mark scope as "exiting"**
- No new tasks may be spawned in this scope
- Queued tasks remain queued
2. **Cancel all non-detached tasks in this scope**
- Send cancellation signal to each Running task
- Remove Queued tasks from queue
- Mark as Cancelled state
3. **Wait for cancellation acknowledgment**
- Block until all tasks reach terminal state (Completed/Cancelled/Failed)
- Timeout: SHOULD have configurable timeout (default: 30s)
- On timeout: Force-kill tasks (implementation-defined)
4. **Execute RAII cleanup**
- Release handles in REVERSE allocation order
- Close channels
- Free memory blocks
- Call destructors if defined
5. **Propagate errors**
- If any task Failed: collect first error
- If Guru present in parent: call Guru
- If no Guru: propagate panic upward
**Example Timeline:**
```
t=0: Scope starts, task1 spawns
t=1: task2 spawns
t=2: Scope exit begins
t=2.1: task1 and task2 receive cancellation
t=2.5: task1 completes cancellation
t=3.0: task2 completes cancellation
t=3.1: RAII cleanup executes
t=3.2: Scope fully exited
```
**Nested Scopes:**
```cymple
outer_scope:
🧵 task_outer()
inner_scope:
🧵 task_inner()
# inner_scope exit: task_inner cancelled
# outer_scope exit: task_outer cancelled
```
MUST cancel in order: innermost to outermost.
---
### Stop Signal (`🛑`) Formal Semantics
**DEFINITION:** Stop signal is a control-flow statement that cancels tasks.
**Scope:** Stop signal is valid ONLY within quantum operation body.
**Semantics (MUST):**
1. **Idempotence**: Multiple `🛑` in same quantum = single stop
```cymple
🛑
🛑 # Second stop has no additional effect
```
2. **Immediate Effect**: Tasks receive cancellation signal synchronously
- Tasks in Running state: Begin cancellation
- Tasks in Queued state: Removed from queue
- Completed tasks: Unaffected
3. **Return Behavior**: Stop causes immediate return from quantum operation
- Progress clause with `🛑`: Returns partial results
- Success clause: Never reached after `🛑`
- Timeout clause: Never reached after `🛑`
4. **Isolation**: Stop affects ONLY the quantum operation it appears in
```cymple
🌀📦 results1 ← [tasks...]
⏩ partial
🛑 # Stops only results1, not results2
🌀📦 results2 ← [other_tasks...]
# results2 unaffected
```
5. **Not Catchable**: Tasks cannot ignore stop signal
- Must honor cancellation
- May execute cleanup (finally blocks)
- Cannot resume after cancellation
**Anti-Example (ILLEGAL):**
```cymple
# ILLEGAL: 🛑 outside quantum operation
🛑 # Error: StopSignalOutOfScope
```
---
### Race Determinism Rules
**Problem:** What happens when multiple tasks complete "simultaneously"?
**RULE (MUST):** Lowest array index wins.
**Formal Definition:**
Given tasks `[t0, t1, t2, ..., tn]` in Race operation:
```
Let C = set of completed tasks at time T
Let winner = min(index(t) for t in C)
Return result of task at index 'winner'
```
**"Simultaneously" Defined:**
Tasks complete simultaneously if their completion timestamps differ by less than the scheduler's time quantum (implementation-defined, typically 1-10μs).
**Example:**
```cymple
🌀⚡ result ← [task0(), task1(), task2()]
✅ winner
↩ winner
# Scenario 1: task1 completes first
# → Return task1's result
# Scenario 2: task0 and task2 complete at T=100μs,
# task1 completes at T=100.5μs
# (within time quantum)
# → All three "simultaneous"
# → winner = min(0, 1, 2) = 0
# → Return task0's result
```
**Justification:** Lowest-index rule ensures:
- Deterministic behavior (testable)
- No race conditions
- Predictable prioritization
---
### Channel Close State Machine
**States:**
```
Created → Open → Closed
↓
Scope Exit (RAII)
```
**State Invariants (MUST):**
1. **Open State:**
- Send: Succeeds (or blocks if full)
- Receive: Blocks until message or close
- Close: Transitions to Closed
2. **Closed State:**
- Send: PANIC (ChannelClosed)
- Receive (buffer not empty): Return buffered messages
- Receive (buffer empty): Return null
- Close: PANIC (AlreadyClosed) OR no-op (implementation choice)
**Transition Rules:**
- `close(ch)`: Open → Closed (explicit)
- Scope exit: Open → Closed (RAII)
- Double close: MUST be deterministic (panic or no-op)
**Buffer Draining:**
Closed channels MUST drain buffer before returning null:
```cymple
📡ch ← 🛰️🔢:3
🚀 ch, 1
🚀 ch, 2
🚀 ch, 3
close(ch)
# Receiver side:
r1 ← 🎯 ch # Returns 1
r2 ← 🎯 ch # Returns 2
r3 ← 🎯 ch # Returns 3
r4 ← 🎯 ch # Returns null (closed + empty)
```
**Loop Termination:**
Loops over channels MUST terminate when closed + empty:
```cymple
🔁 msg in ch
# Receives all buffered messages
# Loop exits when ch closed AND buffer empty
```
---
### Panic and Stack Unwinding
**Panic Sources:**
- Handle validation failure
- Bounds check failure
- Channel closed error
- Explicit throw (`❌`)
**Unwinding Algorithm (MUST):**
```
function panic(error):
1. current_frame = get_current_stack_frame()
2. while current_frame ≠ null:
a. Mark frame as "unwinding"
b. Execute RAII cleanup:
- Get all handles in frame (in REVERSE alloc order)
- For each handle:
* Validate (may skip if already invalid)
* Release resource
* Increment generation
c. Check for Guru:
- If Guru present in this frame:
* Call Guru with error object
* If Guru returns: stop unwinding, resume
* If Guru rethrows (💀): continue unwinding
d. current_frame = parent_frame
3. If no Guru handled:
- Print error to stderr
- Terminate task/program
- Exit code: 1
```
**RAII Order Example:**
```cymple
f1 ← open("a.txt") # Alloc order: 1
f2 ← open("b.txt") # Alloc order: 2
buffer ← allocate(100) # Alloc order: 3
panic()
# Cleanup order (REVERSE):
# 1. release(buffer)
# 2. close(f2)
# 3. close(f1)
```
---
### Task Cap Enforcement
**Definition:** Runtime MUST limit concurrent tasks.
**Algorithm:**
```
global task_count = 0
global task_queue = []
global TASK_CAP = 1000 # Configurable
function spawn_task(task):
if task_count < TASK_CAP:
task_count += 1
start_task(task)
return RUNNING
else:
task_queue.append(task)
return QUEUED
function task_completed(task):
task_count -= 1
if task_queue not empty:
next_task = task_queue.pop_front()
spawn_task(next_task)
```
**Queue Properties (MUST):**
- FIFO ordering
- No size limit (may cause memory pressure)
- Drained on scope exit
**Backpressure:**
Implementations SHOULD provide backpressure when queue grows:
- Warning when queue > 1000
- Error when queue > 10000
- Or configurable thresholds
---
### Error Object Structure (Formal)
**Every panic MUST create error object:**
```
Error {
type: string # Error type name
code: integer # Numeric code (1000-9999)
message: string # Human-readable description
location: string # "file.cym:line:col"
stack_trace: [string] # Function call stack
details: map # Type-specific context
}
```
**Standard Error Codes (MUST):**
| Code | Type | Details Map |
|------|------|-------------|
| 1000 | HandleValidation | {slot, gen} |
| 1001 | StaleHandle | {slot, expected_gen, actual_gen} |
| 1002 | InvalidHandle | {slot, table_size} |
| 1003 | TypeError | {expected, actual} |
| 1100 | BoundsError | {index, size} |
| 1101 | AlignmentError | {address, alignment} |
| 1200 | OutOfMemory | {requested, available} |
| 2000 | ChannelClosed | {channel_id} |
| 2001 | AlreadyClosed | {channel_id} |
| 3000 | TaskLimitReached | {current, cap} |
| 4000 | Timeout | {duration, elapsed} |
**Error Code Ranges:**
Error codes are organized into ranges by category:
| Range | Category | Status |
|-----------|---------------------------|--------------|
| 1000-1099 | Handle Validation | Reserved |
| 1100-1199 | Memory & Bounds | Reserved |
| 1200-1299 | Memory Allocation | Reserved |
| 1300-1999 | *Future: Memory/Safety* | Reserved |
| 2000-2099 | Channel Operations | Reserved |
| 2100-2999 | *Future: Communication* | Reserved |
| 3000-3099 | Task & Concurrency | Reserved |
| 3100-3999 | *Future: Concurrency* | Reserved |
| 4000-4099 | Timing & Timeouts | Reserved |
| 4100-4999 | *Future: Timing* | Reserved |
| 5000-5999 | I/O & Resources | Reserved |
| 6000-6999 | Language & Syntax | Reserved |
| 7000-7999 | Runtime System | Reserved |
| 8000-8999 | Host/FFI Errors | Reserved |
| 9000-9999 | Custom/Implementation | User-defined |
**Notes:**
- Ranges 1000-8999 are reserved for Cymple specification
- Range 9000-9999 is available for implementation-specific errors
- Implementations MUST NOT define errors in reserved ranges
- Future Cymple versions MAY define additional errors within reserved ranges
Implementations MAY add custom error codes in range 9000-9999.
---
**END OF FORMAL SEMANTICS**
This section provides unambiguous semantics for implementation. In case of conflict between this section and examples elsewhere, THIS SECTION IS AUTHORITATIVE.
---
## Modules
**Internal module:**
```cymple
🧩 "math"
🔢result ← sqrt(16)
```
**External plugin:**
```cymple
🛠️ "custom_plugin"
process_data()
```
---
## Foreign Function Interface (FFI)
**NEW in v1.5:** FFI automatically wraps C pointers as Cymple handles.
**v1.6:** FFI linking uses `🔌` (plug into external library), distinct from `🔗` (borrow).
Link to C libraries:
```cymple
🔌 "libc.so.6"
🧵 strlen(s: 🔤) -> 🔢
🧵 c_malloc(size: 🔢) -> 💾 📝 Returns Cymple handle, NOT C pointer
🧵 c_free(h: 💾) 📝 Takes Cymple handle
🔢len ← strlen("Hello")
💬 "Length: 🔢len"
📝 C memory wrapped as validated handles:
💾block ← c_malloc(1024)
📝 Use like any Cymple handle with validation
🔗 block -> B
B[1] ← 42
c_free(block)
```
**Handle Wrapping (CRITICAL):**
- C pointers are **automatically wrapped** as Cymple handles on return
- Cymple handles are **unwrapped** to C pointers when passed to C
- All Cymple safety rules apply (validation, bounds checking)
- C memory must be freed explicitly with C's free function
**Type Mapping:**
- C `int*` → Cymple `🔢` (number, by-value copy)
- C `char*` → Cymple `🔤` (string, copied)
- C `void*` → Cymple `💾` (memory block handle, wrapped!)
- C structs → Pass by copying or via memory block handles
**IMPORTANT:** FFI is an escape hatch. Use with caution. Cymple cannot validate C code's memory safety
---
## Colors & Text
**Command mode:**
```cymple
🎨:🔵
💬 "Blue text"
🎨:⚪ 📝 Reset
```
**Inline mode:**
```cymple
💬 "🔴 Warning ⚪ Normal 🟩 OK"
```
---
## Comments
**Single-line:**
```cymple
📝 This is a comment
```
**Multi-line:**
```cymple
📝
Multi-line comment
Everything indented is a comment
code_continues()
```
---
## Memory Model
### No Garbage Collection
Cymple does **not** use garbage collection:
**Handles:**
- RAII: Freed at block end (OUTDENT)
- Deterministic: Exact timing known
- Thread-local: Each task owns its resources
**Collections:**
- Move-only: Explicit ownership transfer
- Freed when owner goes out of scope
### Isolation Guarantees
1. Each task has its own heap
2. Values transfer only via channels
3. Borrowing ensures exclusive access
4. Move-only prevents simultaneous modification
5. Tasks interact exclusively through channels and events
---
## Symbol Reference
### Basic Language
| Symbol | Meaning |
|--------|---------|
| `←` | Assignment |
| `↩` | Return |
| `❓` | If |
| `⤵️` | Else |
| `🔁` | Loop |
| `📝` | Comment |
| `📘` | Constant |
### Data Types
| Symbol | Type |
|--------|------|
| `🔢` | Number |
| `🔤` | String |
| `✅` | True |
| `✗` | False |
| `📋` | List |
| `🗺️` | Map |
| `🔣` | Bytearray |
| `🧱` | Struct |
### Pattern Matching
| Symbol | Meaning |
|--------|---------|
| `🔀` | Match |
| `➜` | Match arm |
| `_` | Wildcard |
### Tasks & Concurrency
| Symbol | Meaning |
|--------|---------|
| `🧵` | Function/Task |
| `📡` | Channel |
| `🛰️` | Create channel |
| `🚀` | Send |
| `🎯` | Receive |
| `🛑` | Stop/Cancel |
| `🌀⚡` | Quantum race |
| `🌀📦` | Quantum collect |
### Events
| Symbol | Event |
|--------|-------|
| `✅` | Success |
| `❌` | Error |
| `⏩` | Progress |
| `⏹️` | Stopped |
### Memory
| Symbol | Meaning |
|--------|---------|
| `🔗` | Borrow (temporary scoped access to a handle) |
| `🔌` | FFI link (plug into external C library) |
| `release()` | Explicit handle cleanup (use RAII preferred) |
### Timers
| Symbol | Meaning |
|--------|---------|
| `⏱️` | Timeout |
| `⏱️▶` | One-shot timer |
| `⏱️🔁` | Periodic timer |
### Error Handling (Normative)
| Symbol | Meaning |
|--------|---------|
| `🧘` | Guru meditation |
| `⚠️` | Warning |
| `❌` | Error |
| `💀` | Fatal |
---
## Complete Examples (Informative)
### Example 1: Multi-Server Search with Quantum Race
```cymple
🧵 fetch_fastest(🔤query) -> 🔤
🌀⚡ 🔤result ← [
search_eu(🔤query),
search_us(🔤query),
search_asia(🔤query)
]
⏱️ 3s
💬 "All servers too slow"
↩ cached_search(🔤query)
✅ 🔤winner
💬 "Fastest server responded"
↩ 🔤winner
❌ 🔤error
🧘 guru(e)
➜ _
💬 "Search failed: 🔤error"
↩ ""
```
### Example 2: Batch Processing with Progress
```cymple
🧵 process_images(📋files) -> 📋
🔢batch_size ← 10
📋batches ← split_batches(📋files, 🔢batch_size)
🌀📦 📋results ← create_batch_tasks(📋batches)
⏱️ 5m
💬 "Timeout - 📋results.length batches done"
↩ 📋results
⏩ 📋partial every 5
🔢done ← 📋partial.length
🔢total ← 📋batches.length
🔢percent ← (🔢done * 100) / 🔢total
💬 "Progress: 🔢percent%"
🖼️update_ui(🔢percent)
📝 User cancellation
❓ 🔘user_cancelled
💬 "Cancelled by user"
🛑
↩ filter_ok(📋partial)
✅ 📋all
📋ok ← filter_ok(📋all)
📋err ← filter_error(📋all)
💬 "Done: 📋ok.length OK, 📋err.length errors"
↩ 📋ok
❌ 🔤total_failure
💬 "All batches failed"
↩ []
```
### Example 3: Parallel Fibonacci
```cymple
🧵 fib(n: 🔢) -> 🔢
❓ n <= 1
↩ n
🌀📦 📋results ← [fib(n - 1), fib(n - 2)]
✅ 📋done
↩ 📋done[1] + 📋done[2]
🔢result ← fib(10)
💬 "Fibonacci(10) = 🔢result"
```
### Example 4: Timeout Comparison
```cymple
🧵 compare_timeouts()
📝 Milliseconds (default)
🌀⚡ result1 ← [slow_task()]
⏱️ 5000
💬 "Timeout after 5000ms"
📝 Seconds
🌀⚡ result2 ← [slow_task()]
⏱️ 5s
💬 "Timeout after 5 seconds"
📝 Variable (msec)
🔢timeout_ms ← 3000
🌀⚡ result3 ← [slow_task()]
⏱️ 🔢timeout_ms
💬 "Timeout after 3 seconds"
```
### Example 5: Early Cancellation Search
```cymple
🧵 search_until_enough(🔤query) -> 📋
🌀📦 📋results ← [
search_db1(🔤query),
search_db2(🔤query),
search_db3(🔤query),
search_db4(🔤query),
search_db5(🔤query)
]
⏩ 📋partial
💬 "Found: 📋partial.length results"
📝 Stop when we have enough
❓ 📋partial.length >= 20
💬 "Enough results found"
🛑
↩ 📋partial
✅ 📋all
💬 "All databases searched"
↩ 📋all
```
---
## Design Rationale (Informative)
### Why Explicit Type Prefixes?
The emoji type prefixes are mandatory because they:
- Make variables instantly recognizable
- Prevent ambiguity
- Enable better tooling
- Improve readability
- Are a core design principle
### Why No Garbage Collection?
RAII provides deterministic cleanup without GC pauses, making Cymple suitable for:
- Real-time systems
- Embedded systems
- Low-latency applications
- Predictable performance
### Why Share-Nothing Concurrency?
Share-nothing prevents data races by design:
- No locks needed
- No race detectors needed
- Compiler enforces safety
- Simpler mental model
### Why Quantum Operations?
Traditional parallel programming is verbose and error-prone. Quantum operations provide:
- Concise syntax (3-4x shorter than other languages)
- Built-in progress tracking
- Easy timeout handling
- Deterministic behavior
- Consistent error handling
---
## Version History
### Version 2.0 (2026-03-03)
- **RELEASE**: Cymple 2.0 — clean, consistent, one way to do everything
- **REORGANIZED**: Historical versions (1.2–1.5) moved to `archive/`; spec renamed `spec.md`, grammar renamed `grammar.ebnf`
- **CONSOLIDATED**: All 1.5–1.8 breaking-change documentation merged into single "Cymple 2.0 — Breaking Changes" section
### Version 1.8 (2026-03-03)
- **FIXED**: All remaining Memory Block examples updated to 1-based indexing (Generational Handle Validation, Optimization Rules, Borrowing, RAII, FFI sections)
- **FIXED**: README "What's New in Version 1.5" Memory Block examples updated to 1-based
- **FIXED**: README "Project Status" now correctly reports specification version as v1.7
### Version 1.7 (2026-03-03)
- **CHANGED**: Memory Block indexing from 0-based to 1-based — now uniform with all other collections
- **FIXED**: Remaining `🔁 i =` examples in Channel patterns → `🔁 i in`
- **FIXED**: Body text "Cymple 1.5" / "Version 1.5" removed, now version-neutral
- **FIXED**: Malformed guru example `🧘 🔤error` → correct `🧘 guru(e)`
- **FIXED**: Remaining `!=` in pseudocode → `≠`
### Version 1.6 (2026-03-03)
- **REMOVED**: Duplicate range-loop syntax `🔁 i = 1..10` — use `🔁 i in 1..10` only
- **REMOVED**: String `+` concatenation — use interpolation `"🔤var1🔤var2"` only
- **REMOVED**: `!=` not-equal alias — use `≠` consistently (as already in EBNF)
- **CHANGED**: FFI link symbol `🔗` → `🔌` — `🔗` is now borrow-only, `🔌` is FFI-only
- **FIXED**: EBNF `if_statement` else was incorrectly `❌`; now correctly `⤵️`
- **FIXED**: Map `.size` property renamed to `.length` — consistent with all other collections
### Version 1.5 (2025-12-15)
- **NEW**: Automatic `main()` function execution as entry point
- **NEW**: Timeout with time units (`5s`, `500ms`, `2m`, `1h`)
- **NEW**: Progress event frequency control (`every N`)
- **NEW**: Early cancellation with `🛑` in Collect
- **NEW**: Total failure event (`❌ total_failure`)
- **IMPROVED**: Simplified Race (no progress events)
- **IMPROVED**: Consistent `🛑` across quantum ops, channels, loops
- **IMPROVED**: Better error handling in Collect operations
### Version 1.3 (2025-12-02)
- String interpolation
- Else symbol `⤵️`
- Property-based length/size access
- Alternative range loop syntax
- Optional return type for void functions
- Clarified comparison (`==`) and logical operators
### Version 1.2 FINAL (2025-11-28)
- Completed specification
- Production-ready language definition
---
**End of Specification**
Version 1.7 completes the uniformity work: all five indexable types (List, String, Bytearray, Memory Block, and Map key access) now share consistent semantics. Version 1.6 removed all duplicate ways to express the same thing: one loop syntax, one way to build strings, one not-equal symbol, one borrow symbol, one FFI symbol, and one property name for collection sizes. This upholds Cymple's core principles of safety, simplicity, and deterministic execution.
For benchmark comparisons and migration guides, see accompanying documentation.
Source License
MIT License
Copyright (c) 2024 Jörg Burbach
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Source of Truth
This document defines Smallsome 3.0, both modes and inherited semantics.
cymple3.ebnf is the single shared grammar. Website copies are generated
from these files. Existing cymplec, .cy, module identifiers and file
paths remain stable. No second symbolic compiler or runtime is introduced.