Welcome to Dewy
Dewy is a general-purpose programming language designed to make everyday programs direct to write, clear to read, and safe to grow.
The same language should feel comfortable for a short script, a command-line tool, a graphical application, a server, a game, numerical work, or systems software. Dewy starts with convenience rather than ceremony, then uses static types and compile-time reasoning to keep larger programs dependable.
This book teaches Dewy through examples. It begins with the small set of ideas that organize the language, then develops functions, control flow, data, types, modules, and larger programming patterns in an order that lets each chapter build on the last.
What Dewy Feels Like
Dewy favors ordinary expressions that compose instead of a separate syntax feature for every task:
let label = if unread_count =? 0
"Inbox"
else
"Inbox ({unread_count})"
let visible = [
loop message in messages
if not message.archived
message
]
The conditional produces a string. The loop produces the messages that pass its condition, and [] collects them into an array. These are not special “conditional expression” and “list comprehension” sublanguages; they are the same if, loop, and block expressions used everywhere else.
Several principles recur throughout Dewy:
- values copy by meaning, while
@makes intentional shared mutation visible; - functions, objects, and control flow use the same expression grammar;
- strings operate on user-perceived characters rather than exposing UTF-8 bytes by accident;
- ranges state their bounds directly;
- types can express useful facts and guide efficient representations without turning routine code into proof notation;
- domain features such as physical units build on the ordinary type and operator model.
How to Read This Book
Start with Dewy at a Glance for a compact tour, then follow Getting Started if you want to run code. The core chapters are intended to be read in order. Later sections can be used independently once you know the basics.
This book describes the intended Dewy language. When the current compiler has not yet reached a described feature, an unobtrusive note points to Language Design and Compiler Support. Exact syntax and semantic rules live in the Dewy Language Reference.
Dewy at a Glance
Dewy aims to make the straightforward version of a program look straightforward. This tour shows the language's main ideas without trying to teach every rule at once.
Small Programs Stay Small
name = "Dewy"
printl"Hello, {name}!"
Bindings do not require a declaration keyword when the meaning is clear. let and const are available when you want to state mutability explicitly.
let attempts = 0
const limit = 3
attempts += 1
Expressions Compose
Conditionals and blocks produce values:
let access = if signed_in
"account"
else
"sign in"
let circumference = {
let diameter = 2 * radius
pi * diameter
}
Declarations and assignments produce void, so the circumference block expresses only its final calculation.
Functions Read Like Their Calls
let greet = (name:string greeting:string="Hello"):>void =>
printl"{greeting}, {name}!"
greet("Ada")
greet("Grace" greeting="Welcome")
Parameters may be supplied by position or name. Defaults are evaluated for each call that needs them.
One Loop Covers the Common Cases
loop true reconnect() # repeat forever
loop attempts <? limit attempts += 1 # repeat while true
loop task in pending
process(task) # consume an iterator
loop i in 0.. and task in pending
printl"{i}: {task}" # combine iterators
A loop may also express values for a surrounding container to collect:
let active_names = [
loop user in users
if user.active
user.name
]
Text Means User-Perceived Characters
Strings are immutable sequences of Unicode grapheme clusters. Iteration and indexing therefore treat a family emoji or an accented character as one element:
text = "café 👨👩👧👦 🍀"
loop i in 0.. and character in text
if character not =? ' '
printl"{i}: {character}"
Byte and scalar views remain available when a program actually needs those representations.
Values Do Not Alias by Accident
let original = [1 2 3]
let edited = original
edited[0] = 9 # original is still [1 2 3]
Use a place when a function should deliberately update the caller's value:
let reset = (@value:int64):>void => (value = 0)
let count:int64 = 42
reset(@count)
The @ appears in both the function contract and the call, so shared mutation is visible where it matters.
Objects Need No Class Sublanguage
An object is a structural value with named fields. A constructor is an ordinary function that returns one:
let Counter:type = [value:int64 increment:<():>void>]
let counter = (start:int64=0):>Counter => [
value = start
increment = () => (value += 1)
]
let count = counter(40)
count.increment
count.increment
printl"count is {count.value}"
Functions inside an object can use sibling fields directly.
Types Add Meaning Where It Helps
let names:array<string> = ["Ada" "Grace"]
let answer:int64 | none = find_answer()
if answer isnt? none
printl"the answer after this one is {answer + 1}"
Overloads use the same function syntax and are selected by their contracts:
let format = ((value:int64):>string => "integer {value}")
& ((value:string):>string => value)
format(42)
format("already text")
Specialized Domains Use the Same Language
Physical quantities are one example of Dewy's general type model carrying useful facts:
let timeout = 300ms
sleep(timeout)
let distance = 120m
let elapsed = 10s
let speed = distance / elapsed
The unit portion can be checked and simplified at compile time rather than requiring a large runtime wrapper. The same goal—express meaning clearly, prove what can be proved, and keep runtime representation minimal—guides Dewy's facilities for applications, services, systems work, and numerical programming alike.
Continue with Getting Started, or use the Language Feature Index to jump to a particular subject.
Getting Started
This section gets a Dewy program from a source file to a running process.
- Install Dewy, or open the browser playground for µDewy experiments.
- Write and run your first program.
- Continue into the core language with A Few Ideas That Organize Dewy.
Dewy source conventionally uses the .dewy suffix. A directory may begin as one source file and grow into several modules without adopting a separate project language.
The compiler accepts top-level executable code, so small programs do not need a main wrapper. Applications may define main when an explicit entry function is useful.
Current installer, platform, and playground limitations are recorded in Language Design and Compiler Support.
Installation
The current quick installer supports x86-64 Linux with glibc 2.34 or newer:
curl -fsSL https://dewy-lang.org/install.sh | bash
It installs the verified native dewy/udewy pair and its matching library under ~/.dewy; Python is not needed. Open a new terminal and check the installation:
dewy --version
The native compiler is still a development version with some hosted-parity gaps. From a source checkout, the hosted compiler remains available with Python 3.14 or newer:
python -m dewy program.dewy
The browser playground is useful for small experiments without a local installation. It currently runs µDewy, Dewy's bootstrap subset, rather than every construct described in this book.
These are current tooling constraints, not intended language restrictions. See the implementation appendix for context and the website's installation page for up-to-date platform details.
Your First Dewy Program
Create a directory for the program:
mkdir -p ~/code/greetings
cd ~/code/greetings
Create greetings.dewy with this source:
let greet = (name:string):>void =>
printl"Hello, {name}!"
let names = ["Ada" "Grace" "Linus"]
loop name in names
greet(name)
Run it:
dewy greetings.dewy
The output is:
Hello, Ada!
Hello, Grace!
Hello, Linus!
This small program already shows several recurring parts of Dewy:
letintroduces a binding;(name:string):>void => ...defines a function with one string parameter;- whitespace separates array elements, so commas are unnecessary;
loop name in namesconsumes the array from left to right;{name}interpolates a value into a string;printlprints text followed by a newline.
The one-line traditional greeting is valid too:
printl'Hello, World!'
Juxtaposing a callable with its argument can express a call, so this is equivalent to printl('Hello, World!').
Top-Level Code and main
Dewy executes top-level code in source order. A small program therefore needs no special entry wrapper.
When a module declares main, its top level still initializes first and Dewy calls main afterward:
const application_name = "notes"
let main = ():>int64 => {
printl"starting {application_name}"
return 0
}
An explicit top-level main() is an ordinary call; it does not replace automatic entry invocation.
For ordinary use, dewy compiles and runs the program in one command. The hosted compiler lowers Dewy to µDewy, after which the selected µDewy backend produces the executable form.
Stopping to Look: $breakpoint
When a program does something you did not expect, the quickest question is "what are the values here?" Put $breakpoint on that line:
let describe = (hits:array<Hit> label:string):>int64 => {
let total:int64 = 0
loop h in hits {
total += (h.length transmute int64) * 3
$breakpoint
}
return total
}
Each time the program reaches it, it prints the function's live bindings and waits:
── breakpoint at hits.dewy:5 ──
hits = [Hit[length=3 name="a"] Hit[length=10 name="b"]]
label = "run"
total = 9
h = Hit[length=3 name="a"]
>>>
Press Enter (or type \c) to continue to the next stop, \q to quit, \h for the list. Commands start with a backslash because the plain prompt is reserved for expressions: a compiled program cannot evaluate a typed-in expression yet, so for now it says so. Output that is not a terminal (a pipe, /dev/null) continues on its own, so breakpoints left in do not hang an unattended run.
For stepping through code, breakpoints on lines you did not edit, and the call stack, run the program under a debugger:
dewy debug hits.dewy
That makes a debug build and opens gdb (or lldb) on it. The debugger knows Dewy files, lines, and values — b hits.dewy:5, n, bt, and frame variable or p total show the bindings as Dewy prints them — and a $breakpoint in the program stops the debugger on that line instead of prompting. The same works inside Cursor or VS Code with the Dewy extension and CodeLLDB: breakpoints in the gutter, stepping, and the Variables pane. See Debugging in the Reference for the details and the editor configuration.
A Few Ideas That Organize Dewy
Dewy has a broad feature set, but a small number of ideas explain how those features fit together.
Expressions Produce Values
Literals, calls, blocks, conditionals, and loops are expressions. An expression may produce one value, several values for a surrounding construct to collect, void, or no possible value at all.
This is why Dewy does not need a separate ternary operator, loop-capture grammar, or statement-only form of control flow.
One Grammar, Reused
The meaning of a construct comes from the expressions inside it and their types:
[]can collect array elements, object fields, or key/value pairs;- juxtaposition can call a function, index a value, or multiply compatible quantities;
{}creates a scoped block wherever a block is needed;=>builds a function from a parameter contract and a body.
These uses are not chosen by textual guesswork alone. Parsing preserves meaningful alternatives and semantic analysis resolves them from context and types.
Values by Default, Places by Request
An ordinary assignment or function argument supplies an independent value. The compiler may move or share storage when that cannot be observed, but source code does not accidentally create two mutable names for one value.
@ requests a place when shared mutation is the point of the operation. Both the function signature and call site show that choice.
Meaning and Representation Are Separate
A type describes what a value means. Its runtime representation is a compiler decision as long as the program cannot observe a difference.
An integer may have arbitrary-precision semantics while range analysis proves that a machine-width representation is sufficient. A physical unit may participate in type checking and then disappear entirely. An array descriptor may be omitted when its length and layout are already known.
Compile Time Is Ordinary Language Territory
Types, import paths, dimensions, and other compile-time values use Dewy's expression model instead of separate mini-languages. Not every general compile-time operation is designed or implemented yet, but the organizing rule is that compile-time facilities should compose with the rest of Dewy rather than forming an unrelated macro language.
The next chapter starts with the first of these ideas: expressions and the values they produce.
Expressions, Produced Values, and Blocks
Dewy is expression-based. A literal, call, conditional, block, or loop can participate in a larger expression when it produces a value.
let answer = 42
let larger = answer + 1
let label = if larger >? 40 "large" else "small"
Values, void, and never
Most expressions produce a value. Some operations perform useful work without producing one; their type is void.
Declarations, ordinary assignments, and printl are common void expressions:
let count = 0 # void
count += 1 # void
printl"ready" # void
never describes a path that cannot complete normally, such as an exit operation. It is not another spelling of void.
none is different again: it is a real value that can be stored in an optional type. Optional Values and Narrowing develops that distinction.
Suppressing a Value
An attached semicolon evaluates an expression but suppresses what it would otherwise produce:
let selected = [
load_primary();
load_secondary()
]
Both calls run, but the array collects only the value from load_secondary().
The semicolon is attached to the expression it suppresses. It is not general statement-ending punctuation. An unattached semicolon is reserved for selecting another array dimension.
Blocks
{} forms a scoped block. Expressions inside run from top to bottom, and the block expresses their non-void results:
let circumference = {
let diameter = 2 * radius
pi * diameter
}
The declaration is void, so the block produces only pi * diameter. diameter belongs to the child scope and is not visible afterward.
Parentheses also group expressions, but do not create a child lexical scope:
let result = (1 + 2) * 3
A block can express several values when its surrounding context knows how to collect them:
let digits = [{ 1 2 3 }]
Loops use the same rule, which is why an array-producing loop needs no separate comprehension syntax. Loop Capture falls out for free.
Comments
# begins a line comment. #{ ... }# is a nestable block comment:
# one line
#{
an outer comment
#{ with a nested comment }#
}#
Comment markers inside strings are ordinary string contents.
The Reference specifies evaluation behavior and operator grouping precisely.
Bindings and Scope
A binding attaches a name to a value. let is mutable. const is not. Assigning to a name that has no visible binding implicitly creates a let.
let mutable = 1
const fixed = 2
inferred = 3 # same as let inferred = 3
mutable = 5 # updates the original `mutable`
mutable += fixed
inferred = inferred + 1
Type annotations go after a colon:
let count:int = 10
const name:string = 'Dewy'
let Pair:type = [left:int64 right:int64]
Unpacking
A […] of names on the left of = takes a value apart. An object unpacks by field name: each target takes the field of that name, in any order, and fields you leave out are simply not taken. Arrays unpack by position, and so do dictionaries and sets, in insertion order: every element must be named, _ discards one, and a nested […] unpacks an element further — a dictionary's entries are [key value] pairs. let (or const) declares the names; a bare unpack declares the new ones and assigns the ones already in scope, like any bare a = ….
let Hit:type = [length:int64 name:string]
let hits:array<Hit> = [Hit[length=3 name="a"] Hit[length=10 name="b"]]
[name length] = hits[0] # by name; the element is read once
let [first _ last] = [10 20 30] # by position
[first last] = [last first] # a swap: both names exist, so both are assigned
let ages = ['ann' -> 30 'bob' -> 7]
let [[k1 v1] [k2 v2]] = ages # entries in insertion order
let [m1 m2] = set[7 8] # members in insertion order
Positional unpacking needs the count to be known: a literal, an exact-length annotation, or a growable array (or dictionary) whose length is a proven fact at that point (xs.push steps it). A runtime-length container is an error there — index or look up what you need under a guard instead.
Scope
A name is visible in the { } block where you declared it, and in any child blocks. A name declared inside can hide an outer one and disappears when the block ends. ( ) does not start a new scope. It shares the surrounding one.
let x = 1
{
let x = 2 # shadows the outer x
printl'{x}' # 2
}
printl'{x}' # 1
Code that runs right away cannot read a name before it is set. Function bodies may use names declared later in the same scope. The compiler checks that the name is set at each place that can actually call the function. That is how two functions can call each other, and why a helper can sit below the function that uses it.
let first = ():>int64 => second()
let second = ():>int64 => 20
printl(first()) # ok to call at this point
Types and Numbers
Dewy checks types when it compiles. Literals and surrounding context usually provide enough information that annotations can stay focused on interfaces and important guarantees.
let count = 10
let enabled = true
let name:string = "Dewy"
Integers
int is a signed integer with arbitrary-precision semantics. uint is nonnegative. Fixed-width types are available when width is part of an interface or representation:
let offset:int32 = -12
let byte:uint8 = 255
let counter:uint64 = 1
An integer literal begins as the exact number written. Context can place it in a compatible integer type, but an out-of-range literal is rejected instead of truncated.
Fixed-width arithmetic retains its width and rolls over according to that bit representation. int does not acquire overflow merely because the compiler proves that a machine integer is an efficient representation for a particular program. The compiler stores an int as a 64-bit word when range analysis proves it fits, and as an arbitrary-precision big integer otherwise — the program's meaning does not change, only its cost. dewy analyze prints a representation report listing every place a big integer was chosen and why. When you want arbitrary precision regardless of what the analysis can prove, annotate bigint:
let main = ():>int64 => {
let seed = 3000000000
let cube = seed * seed * seed # 2.7e28: a big integer automatically
let factor:bigint = 2^100 # always a big integer
printl"{cube} {factor}"
return 0
}
Big values stay inside boundaries that admit them: returning one from a function whose result is int64, or passing it to a word-sized parameter, is a compile error until a comparison proves the range or the signature says bigint.
Rationals and Fixed-Point
Dividing integers with / produces an exact rational, and a decimal literal is an exact rational too:
let third = 1/3 # rational
let price = 9.8 # 49/5, exactly
let sum = third + 2/3 # 1
let ratio = (2/3)^2 # 4/9
Rationals print as n/d (or as an integer when whole). // remains floor division on integers. A literal zero divisor is a compile error.
fixed is a fixed-point number with 32 fraction bits, the representation trigonometry produces. A fixed value absorbs integers and rationals in arithmetic, and constants convert exactly:
let x:fixed = 1/3
let y = x * 2 + 0.25
^ raises integers and rationals to integer powers: constant powers fold, a negative constant exponent yields a rational (2^(-3) is 1/8), and a runtime exponent must be a constant or unsigned so that an integer result is sound.
Dewy's initial focus is on intuitive everyday arithmetic with integers, exact rationals, and fixed-point values. First-class IEEE floating-point types and arithmetic are planned as well, tentatively alongside the full matrix math system, to support conventional scientific, array, and tensor computing. Floating-point arithmetic is not implemented yet; its eventual role extends beyond host interoperability.
Booleans
bool has the values true and false:
let ready = true
let retry = failed and attempts <? limit
English Boolean operators include and, or, not, nand, nor, xor, and xnor.
void, never, and none
voidmeans an expression completed without producing a value.nevermeans the path cannot complete normally.noneis a storable value used for missing alternatives.
T | none is an optional value, covered in Optional Values and Narrowing.
Type Values and Aliases
A type is a compile-time value of type type. Bind it to a name for reuse:
const Count:type = int
const Pair:type = [left:int64 right:int64]
let total:Count = 3
let origin:Pair = [left=0 right=0]
<> groups a type expression where ordinary expression context would be ambiguous:
const SmallPrime = <2 | 3 | 5 | 7>
const Result = <string | none>
Literal values can therefore participate in types. | forms a union of alternatives.
Creating Nominal Types
type of Parent creates a fresh nominal child of an existing type:
const UserId:type = type of int
const MyCustomError:type = type of error
Each evaluation of type of creates identity. Ordinary type intersection does not:
const ContextError:type =
(type of error) & [context:string]
const DetailedContextError:type =
ContextError & [source:string]
DetailedContextError adds a structural requirement while retaining ContextError's nominal ancestry. It is not another nominal error variant.
When an alternative belongs to the nominal exception family, navigation can forward that exception value while applying the requested member operation to the ordinary alternatives. Both error and none descend from exception. This is a rule for exception-classified union members, not for arbitrary unions.
Parameterized Type Aliases
A parameterized alias accepts compile-time type arguments. A bound such as <T of real> constrains the accepted argument; unlike the expression type of Parent, it does not create nominal identity:
const Duration:type = <T of real>(T * Time)
let pause:Duration<int64> = 300ms
Broader Numeric Domains
Provisional design: Beyond integers, rationals, and fixed-point, Dewy's numeric hierarchy is intended to include reals, complex values, and quaternions. Their construction, promotion, rounding, and literal rules are not yet specified, so this book does not invent syntax for them.
The Reference defines the settled numeric rules and type/conversion model.
Functions and Calls
A function combines a parameter contract with a body expression:
let greet = (name:string):>void =>
printl"Hello, {name}!"
:>void is the return contract. Dewy can infer a result from a body whose parameter types are already known:
let square = (value:int64) => value^2
let add = (left:int64 right:int64) => left + right
One parameter can omit parentheses. Zero parameters use ().
Positional and Named Arguments
Ordinary parameters may be supplied by position or name:
let describe = (name:string count:int64):>string =>
"{name}: {count}"
describe("messages" 3)
describe(count=3 name="messages")
Dewy processes arguments from left to right. A positional argument fills the first parameter still open by position; a named argument fills that name.
Defaults Are Per Call
A default is used only if the completed call leaves its parameter unset:
let greet = (name:string greeting:string="Hello"):>void =>
printl"{greeting}, {name}!"
greet("Ada")
greet("Grace" greeting="Welcome")
greet("Linus" "Hi")
The default expression evaluates separately for every call that needs it. A mutable value created by a default is not shared between callers.
A default does not remove its position. This matters when a required parameter follows one:
let combine = (left:int64 scale:int64=2 right:int64):>int64 =>
left + scale * right
combine(10 3 16)
combine(10 right=16)
combine(10 16) supplies left and scale, then reports that right is missing.
Keyword-Only and Position-Only
A bare ... ends the positional run:
let connect = (host:string ... timeout_ms:int64):>void => {
# ...
}
connect("example.test" timeout_ms=2_000)
Wrapping a parameter in <> makes its name private to the function's body and requires callers to use its position:
let increment = (<value:int64>):>int64 => value + 1
increment(41)
increment(value=41) is an error. A bare identifier in a function literal is always a parameter name, not an anonymous type annotation.
Function Contracts
A function type writes the same interface without a body and can be used anywhere another annotation can:
let apply = (
transform:<(value:int64):>int64>
value:int64
):>int64 => transform(value)
Names in a contract determine which keyword calls it accepts. Position-only parameters use the same <name:type> form in a function type as in a function literal. Dewy does not reinterpret a bare identifier as an unnamed type annotation.
A function that can fail lists its error values directly among its return alternatives, such as :>Customer | NotFoundError. There is no additional result wrapper around a successful return.
Overloads
& combines functions into an overload set. Argument contracts select the applicable alternative:
let format = ((value:int64):>string => "integer {value}")
& ((value:string):>string => value)
format(42)
format("already text")
An unmatched or ambiguous call is an error.
Pipes
Pipes are calls written in data-flow order. The right operand is an ordinary expression, so a named function is selected with @ (bare, it would be called); a function literal pipes as it is:
3 |> @square
("Grace" greeting="Welcome") |> @greet
3 |> (x:int):>int => x * x
Grouping several piped arguments keeps any named bindings local to the group.
Functions as Values
Functions can also be passed, stored, and configured for a later call. Because a bare function name is always a call — a function with required parameters cannot even be mentioned without them — Dewy uses @ to select the function itself.
That topic builds on places, object members, and grouping, so it is developed later in Function Values and Composition. The Reference defines exact argument binding independently of those function-handle details.
Branching and Flow Control
if Expressions
An if chooses the first body whose condition is true:
if temperature <? freezing
printl"solid"
else if temperature <? boiling
printl"liquid"
else
printl"gas"
Because it is an expression, an exhaustive conditional can produce a value:
let phase = if temperature <? freezing
"solid"
else if temperature <? boiling
"liquid"
else
"gas"
The alternatives must produce compatible types. A conditional without else normally produces void, because no body may run.
Blocks as Alternatives
Use {} when an alternative needs several operations:
let result = if cached isnt? none {
record_hit()
cached
} else {
let loaded = load()
record_miss()
loaded
}
Declarations and bookkeeping assignments are void; each block produces its final value.
Narrowing Along the Flow
Conditions establish facts inside their bodies:
let answer:int64 | none = lookup()
if answer isnt? none
printl"next is {answer + 1}"
Earlier failed alternatives also establish facts in later ones. Assignment or a call that may mutate the tested value invalidates facts that are no longer guaranteed.
A chain of is? tests that covers every alternative of a union is exhaustive, so it needs no else — whether it returns or produces a value:
let describe = (v:int64 | string):>int64 => {
if v is? int64 {
return v * 2
} else if v is? string {
return v.length
}
}
When a value-producing chain misses an alternative, the error names it: `none` is not handled by any `is?` arm.
match
When a value has several shapes, match writes the cases as signatures: an arm pattern => body matches when the value would satisfy that parameter list.
let describe = (v:bool|int64|string):>string => match v {
<bool> => "a flag" # a type narrows and binds nothing
answer:42 => "the answer" # a singleton
small:int64<small <? 100> => "small" # a refinement is the arm's guard
n:int64 => "large" # `n` is the value at `int64`
s:string => s
}
Object shapes bind fields ([sign:1 limbs] => limbs.length on a bigint), sequences match element-wise (match (x y) (a:int64 b:int64) => a + b), and a bare name is the catch-all that binds the whole value — _ idiomatically; another name warns, since it shadows whatever it meant (write n:T to bind with a type). else attaches outside the arms and chains with if as usual. A chain with a match must be total — the arms cover the type, guards included when the type lets them (<? 0 and >=? 0 cover int64; over -1|0|1, <? 0 and >? 0 miss 0, and the error says so) — or end in else; an arm nothing can reach is an error too.
Short-Circuit Conditions
and evaluates its right side only if the left side succeeds. or evaluates its right side only if the left side fails. nand, nor, xor, and xnor follow their Boolean definitions.
This makes guarded use concise:
if user isnt? none and user.active
open_dashboard(user)
Exiting Control Flow
return exits the current function. break exits a loop, and continue begins its next condition evaluation.
let find = (items:array<int64> wanted:int64):>int64 | none => {
loop item in items
if item =? wanted
return item
return none
}
Labeled exits can target an enclosing loop; Loops and Multiple Iterators develops those forms. Errors as Values later introduces the corresponding concise form for passing an exception value back to the caller.
Related Control-Flow Designs
Provisional design: General pattern matching, unconditional cleanup/finally behavior, and transformed error propagation must compose with these expression and narrowing rules. Their complete surface syntax is not yet specified.
Optional Values and Narrowing
none is a real value representing a missing alternative. It is not void or an uninitialized name.
An optional type is a union with none:
let answer:int64 | none = lookup_answer()
Checking an Optional
Use is? or isnt? to establish which alternative is present:
if answer isnt? none
printl"the next answer is {answer + 1}"
Inside the body, answer is known to be int64. An early exit can establish the same fact afterward:
if answer is? none
return
printl"answer is {answer}"
value is? Type tests membership in a type. Literal alternatives can be tested directly as well.
Equality against a value of one alternative asks both questions at once: answer =? 3 is true when answer holds an int64 equal to 3, and false when it is none (so answer not=? 3 is true then). answer =? none is the same test as answer is? none. Equality narrows like a test: inside if answer =? 3, answer is an int64 whose value is known to be 3 (so it even fits an int8), and the else branch of answer not=? 3 knows the same.
let answer:int64|none = 3
if answer =? 3 { let small:int8 = answer printl"{small}" }
let words:array<string|none> = ["a" none]
if words[1] =? none { printl"missing" }
Producing Optional Values
A function can return an optional explicitly:
let choose = (enabled:bool):>int64 | none =>
if enabled 42 else none
Any expression whose alternatives include a value and none can produce an optional.
Absence Is an Exception, Not an Error
none says that a value is absent. An error says that an operation failed and carries its own error type. Both descend from Dewy's exception type family, so navigation forwards either one without trying to access a member on it. They nevertheless remain distinct contracts:
User | none # a user may simply be absent
User | NotFoundError # looking up the user may fail
Exception values automatically forward when they are encountered as the receiver of a navigation route:
let user:User | none = findUser(id)
let city = user.profile.address.city
# city has type string | none
If users want a non-forwarding sentinel, they can define an ordinary type that does not descend from exception. Every ordinary alternative must support a requested member or be narrowed away first.
This is ordinary typed value flow, not a hidden throw or stack unwind. The next chapter develops the exception family and propagation in full.
General unions with several unrelated runtime layouts use the same type-theoretic model, though their complete representation and narrowing support is still a design and implementation frontier.
Errors as Values
Dewy represents an expected failure as an ordinary value. A function lists its successful result and its possible errors directly in one union:
let loadCustomer = (id:CustomerId)
:> Customer | NotFoundError | DatabaseError
=> {
# ...
}
Calling loadCustomer produces one of those three values. There is no Result container to unwrap and no Ok or Err constructor around either outcome. Error types belong to Dewy's nominal error family, which lets the language distinguish failures from ordinary domain alternatives.
Public functions should normally state a stable set of errors in their return contract. A private helper may allow the compiler to infer them.
Dewy never traps. A running Dewy program has no hidden exits: it stops only where you wrote
return, an explicit exit, or a$runtime_assertof your own. Whatever could fail is either proven safe at compile time (and compiles to nothing) or comes back to you as a value in the type —rational | Overflow,T | none, your own error types — for you to handle. There is no third option, in your code or in the library — and if you write a library yourself, the same courtesy applies: don't exit on your callers' behalf. Ask for the proof in a parameter's type, or hand the failure back in the result.
Exception Values Forward
Safe navigation is governed by a broader exception type family. Both error and none descend from exception, and programmers can define other exception types. Any alternative in this family forwards through navigation.
Here, “exception” describes an ordinary value's type. It does not imply throwing, catching, or stack unwinding.
Defining Exceptions
type of Parent creates a fresh nominal child. A unit-like error has one canonical inhabitant written with the type's own name:
const MyCustomError:type = type of error
let maybeNumber = ():>int64 | MyCustomError => {
if random.coinflip
return MyCustomError
return 42
}
There is currently no separate MyCustomError() spelling. Whether the canonical inhabitant and its type value are literally the same semantic object is left open.
An error that carries context combines its fresh nominal identity with a structural type:
const MyComplexError:type =
(type of error) & [extra:string fields:int64]
let problem = MyComplexError[
extra='some extra context'
fields=42
]
Only type of error creates identity. A later alias such as MyComplexError & [metadata:string] adds a structural requirement while remaining the same nominal error kind.
Safe Navigation
When a receiver might be an exception, Dewy applies a member operation to every ordinary alternative and forwards the exception unchanged:
let DatabaseError:type = type of error
let Address:type = [city:string]
let Profile:type = [address:Address|none]
let Customer:type = [profile:Profile]
let find_customer = (id:int64):>Customer | DatabaseError | none =>
if id >? 0 [profile=[address=[city="paris"]]] else DatabaseError
let city_of = (id:int64):>string | DatabaseError | none => {
let customer = find_customer(id)
let city = customer.profile.address.city
return city # string | DatabaseError | none
}
If customer is a Customer, the route reads its profile, address, and city. If it is a DatabaseError or none, none of those accesses run; that exception becomes the value of city. Every later access in the route follows the same rule, so Dewy does not need a separate ?. operator.
This behavior is type checked rather than based on whether a value happens to be truthy. Every non-exception alternative must support the requested member:
let subject:Customer | Organization | DatabaseError = findSubject(id)
let name = subject.name
This is valid only if both Customer and Organization have a usable name. An ordinary union member is never silently skipped.
If a program needs a sentinel that does not forward, it defines an ordinary type that does not descend from exception. Such a sentinel must be narrowed explicitly before accessing members that it does not support.
Propagating an Exception
Use or_throw when the current function should pass an exception back to its caller:
let loadGreeting = (id:CustomerId)
:> string | NotFoundError | DatabaseError
=> {
let customer = loadCustomer(id) or_throw
return "Hello, {customer.name}!"
}
The expression evaluates loadCustomer(id) once. A Customer becomes the local customer; an exception returns immediately from loadGreeting. The enclosing return contract must accept every exception alternative that can be forwarded this way. This applies to none and user-defined exceptions as well as errors.
Unlike navigation on a receiver, arguments do not forward implicitly:
let amount:Money | ParseError = parseMoney(text)
invoice.setAmount(amount) # type error
invoice.setAmount(amount or_throw) # passes Money or returns ParseError
Keeping arguments explicit prevents a call from acquiring hidden early exits for any exception-bearing expression supplied to it.
Inspecting and Recovering
An error is still a value, so ordinary type tests can narrow it:
let customer = loadCustomer(id)
if customer is? NotFoundError
return guestCustomer
else if customer is? DatabaseError
return customer
printl"Welcome back, {customer.name}!"
After both error alternatives have left the flow, customer is known to be a Customer. General pattern selection and concise type-directed recovery helpers are planned, but their final syntax is not yet fixed.
Not every alternative that describes an unsuccessful search should be an exception. User | Missing contains two ordinary domain outcomes and does not gain forwarding. User | NotFoundError says that the second alternative is an error intended to participate in propagation.
What the Compiler Does Today
The current compiler implements the core of this chapter: unit-like error types minted with type of error, errors as ordinary union alternatives, is? to handle them (is? error covers the whole family), and or_throw to propagate. Errors carrying fields and forwarding member access are still design.
let NotFound:type = type of error
let Invalid:type = type of error
let lookup = (id:int64):>int64 | NotFound | Invalid => {
if id <? 0 { return Invalid }
if id >? 100 { return NotFound }
return id * 2
}
let twice = (id:int64):>int64 | NotFound | Invalid => {
let first = lookup(id) or_throw # propagate NotFound / Invalid
return lookup(first) or_throw
}
let main = ():>int64 => {
let r = twice(30)
if r is? error { return 1 } # NotFound: 60 -> 120 is out of range
return r # the ordinary alternative
}
Errors, Absence, and Effects
none represents absence. It is not an error, but it is an exception, so optional navigation forwards it automatically. This makes T | none the common option-like form: use the T normally, or carry its exceptional absence through the route. Optional Values and Narrowing covers explicit tests and fallbacks.
Errors are also separate from effects. An error appears in the returned union because it is a value the caller receives. Effects describe behavior such as I/O, blocking, or mutation even when a call succeeds.
Design boundary: Direct error unions, nominal exception creation, the
exceptionforwarding family, safe receiver navigation, explicit argument handling, and the separation of errors from effects are the intended model. Transforming an exception duringor_throw, pattern matching, recovery helpers, and extending automatic forwarding to pipelines remain provisional.
The Errors and Forwarding reference gives the exact type rules and collects the remaining open points.
Loops and Multiple Iterators
Dewy uses one loop expression for repetition. Its condition can be an ordinary Boolean expression, an iterator clause, or a logical formula made from iterator clauses.
Repeating While a Condition Holds
The condition is evaluated before each iteration:
let attempts = 0
loop attempts <? 3 {
reconnect()
attempts += 1
}
Use loop true for repetition that ends through break or return:
loop true {
let message = receive()
if message is? none
break
handle(message)
}
Consuming an Iterable
In a loop condition, name in iterable advances the iterable and binds its next value to name. The body runs when a value was produced:
loop fruit in ["apple" "banana" "peach"]
printl"I like {fruit}."
Ranges are iterables. Integer ranges use a unit step unless a second anchor states another step:
loop number in 1..5
printl(number)
loop even in 0,2..10
printl(even)
loop descending in 5,4..0
printl(descending)
0.. has a first value and no right bound, so it can iterate indefinitely. A left-unbounded range such as ..10 has no first value and cannot be iterated. Ranges covers bounds and steps in detail.
Combining Iterators
Iterator clauses combine with the ordinary logical operators. and provides the familiar zip behavior: every required iterator advances once, and the loop ends when the formula becomes false.
let names = ["Alice" "Bob" "Charlie"]
let colors = ["red" "blue" "green" "yellow"]
loop name in names and color in colors
printl"{name} chose {color}."
Pairing a finite source with a right-unbounded counter provides enumeration without a separate construct:
loop index in 0.. and fruit in ["apple" "banana" "peach"]
printl"{index}: {fruit}"
For a multiiterator formula, every iterator leaf advances once from left to right before the logical formula is evaluated. This is deliberately different from ordinary Boolean short-circuit evaluation: skipping a leaf would make its position drift relative to the others.
or continues while either source produces a value. A target that can be exhausted during a body iteration has optional type T | none:
loop left in left_items or right in right_items {
if left isnt? none
process_left(left)
if right isnt? none
process_right(right)
}
This applies the same narrowing rules introduced in Optional Values and Narrowing. By contrast, and stops before a required source's missing value reaches the body.
The same rule extends to xor, nand, nor, and xnor: each iterator contributes the Boolean result of its current advance, and the operator's truth rule decides whether the body runs. Some formulas remain true after every input is exhausted. For example, xnor of two exhausted iterators is true, so such a loop needs another exit if it can reach that state.
Provisional design: Combining iterator clauses with ordinary Boolean predicates is a separate case from a formula containing only iterator leaves. Its advancement and short-circuit rules have not been selected, so this book does not infer behavior for expressions such as
item in items and clock.now <? deadline.
Exiting a Loop
break leaves the nearest loop. continue starts its next condition evaluation. return leaves the containing function.
loop task in tasks {
if task.cancelled
continue
if shutting_down
break
process(task)
}
A scope metatag can name its directly contained loops so an exit can target one through nested control flow:
{
$rows
loop row in rows {
loop column in columns {
if retry_row()
continue $rows
if complete()
break $rows
process(row column)
}
}
}
The label belongs to the scope, not textually to the next loop. It cannot duplicate or shadow an active label, and labels do not cross function boundaries.
Loop Capture
A loop expresses the non-void values produced by its body. Surrounding [] collects them into an array; this is loop capture:
let squares = [
loop number in 1..5
number^2
]
A body may produce no value on some iterations, which makes filtering use the same ordinary if expression:
let odd = [
loop number in 1..10
if number % 2 =? 1
number
]
Nested collectors build nested arrays:
let table = [
loop row in 1..3 [
loop column in 1..3
row * column
]
]
Iterating Dictionaries and Sets
loop [key value] in dictionary unpacks each entry in insertion order, and loop member in set visits each member in first-seen order. The iterated key or member is a proven key inside the body, so dictionary[key] needs no check there. A loop must not change the container it iterates — stores, pop, and clear on it inside the body are compile errors, the static counterpart of Python's "changed size during iteration".
Provisional design: General destructuring in iterator targets and collecting dictionary or multidimensional results must extend this model without creating a separate loop grammar. Their complete binding and shape rules are still being designed.
The Reference defines the exact iterator advancement and exhaustion rules.
Ranges
A range is a span over numbers, characters, or another ordered type. Ranges show up in loops, indexing, and membership tests.
A range always contains ... Endpoints juxtapose with ... An optional first,second pattern sets the step size.
Syntax
[first..] # first to inf
[..last] # -inf to last
[first..last] # first to last
[..] # -inf to inf
[first,second..] # step is second - first
[first,second..last]
[..2ndlast,last] # step is last - 2ndlast
[first..2ndlast,last] is not allowed. Use [first,second..last] instead.
The inferred step may be positive or negative. A zero step such as 1,1..10 is invalid.
Bounds are inclusive by default. Square brackets include an end; parentheses exclude it. The two ends are independent.
[first..last] # include both
[first..last) # include first, exclude last
(first..last] # exclude first, include last
(first..last) # exclude both
first..last # same as [first..last]
Juxtaposition
An endpoint is part of the range only if it is juxtaposed with ..:
first..last # first to last
first ..last # -inf to last
first.. last # first to inf
first .. last # -inf to inf
Range juxtaposition is medium-low precedence, so first..last + 1 is first through last+1.
first..last+1
first,second..last/2
a in first..last
Numeric Ranges
(1..5) # 2 3 4
(1..5] # 2 3 4 5
[1..5) # 1 2 3 4
[1..5] # 1 2 3 4 5
1..5 # 1 2 3 4 5
A right-unbounded range such as 0.. has a first value and iterates forever. A left-unbounded range such as ..10 is a valid range value but cannot be iterated, because it has no first value. The same is true of .. and ..3,5.
Character Ranges
Unannotated string bounds use one-grapheme strings. Iteration is defined when each supplied anchor is a grapheme containing exactly one Unicode scalar. Values advance in scalar order and skip the surrogate interval.
ord_range = 'a'..'z'
alpha_range = ['a'..'z'] + ['A'..'Z']
loop letter in 'z','y'..'a' { ... }
let ascii_scalars:range<uint32> = 'A'..'Z'
Multi-scalar graphemes have no invented universal successor. Enumerating them requires an explicit alphabet or collation policy. See Strings and Graphemes.
Uses
Loops
loop i in 0..5 print'{i} '
# 0 1 2 3 4 5
loop i in 5,4..0 print'{i} '
# 5 4 3 2 1 0
A reversed range requires an explicit step. 5..0 results in an empty range.
Membership
5 in? [1..5] # true
5 in? (1..5) # false
3 in? (1..5) # true
Indexing
full_string = 'this is a string'
substring = full_string[3..12]
printl(substring) # 's is a str'
Because indexing is juxtaposition, the range's own brackets choose inclusive or exclusive ends:
full_string(3..12) # ' is a st'
full_string[3..12) # 's is a st'
full_string(3..12] # ' is a str'
full_string[3..] # 's is a string'
full_string[..12] # 'this is a str'
full_string[..] # the whole string
end is the index of the last element:
arr[end] # last element
arr[end-1] # second to last
arr[..end-3]
arr[5..end-3]
arr[end-3..]
Provisional design: Integer positions and
enddefine ordinary sequence slicing. Indexing by noninteger ordered domains requires a collection-specific indexing contract and is not implied by the generic range syntax.
Range Arithmetic
Provisional design: Applying arithmetic to a complete range and combining several ranges are selected directions, but their result types, normalization, empty-span behavior, and runtime representation are not yet fully specified. The following examples illustrate that direction rather than defining the remaining edge cases.
loop i in [0..4]/4 print'{i} '
loop i in [0..4]*0.25 print'{i} '
# both: 0 0.25 0.5 0.75 1
This expresses the same intended values as [0,0.25..1]. Numerical libraries can provide linspace and logspace helpers without changing the range grammar.
Compound Ranges
complex_range = [1..5] + (15..20)
loop i in complex_range
printl(i) # 1 2 3 4 5 16 17 18 19
7 in? complex_range # false
16 in? complex_range # true
complex_range = [1..20) - (5..15]
Values, Copies, and Places
Dewy uses value semantics by default. Assigning, passing, or returning a value gives the destination an independent value:
let original = [1 2 3]
let copy = original
copy[0] = 9
# original is still [1 2 3]
The compiler does not need to physically copy every byte. It may move storage, borrow it for reading, or share immutable backing data whenever the program cannot observe a difference.
Asking a Function to Update Your Value
When mutation should be visible to the caller, pass a place with @. The parameter also carries @:
let increment = (@value:int64):>void => (value += 1)
let count:int64 = 41
increment(@count)
printl"{count}" # 42
Both sides advertise the mutation. increment(count) supplies a copy and does not satisfy a place parameter.
A Place Can Follow a Route
@ appears only at the beginning of a route and selects the place at the end of that entire route:
set(@point.x)
set(@values[i])
set(@grid.rows[row][column])
The parser first groups the prefix as (@point).x, but that intermediate grouping is not the semantic boundary: @point.x refers to the place occupied by x, not first to a standalone reference value for point. Putting the route inside the prefix, @(point.x), selects the same place. Grouping the completed selection as (@point.x) also preserves that place while ending the @ chain, which matters when a following argument group calls a selected function. There is no point.@x spelling.
A computed index evaluates once before the call.
Whole-Value Replacement
A place can expose the entire selected value, not only its scalar fields:
let replace_pair = (@pair:Pair):>void => {
pair = [left=20 right=22]
}
replace_pair(@pairs[index])
For a recursively fixed aggregate, the caller can provide the complete destination storage. Runtime-sized replacements need the broader ownership and escape design described in the implementation appendix.
Preventing Conflicting Mutation
A call cannot receive two mutable places that may overlap:
set_both(@pair.left @pair.right) # distinct fields
set_both(@values[0] @values[1]) # distinct constant indices
Prefix routes overlap, and dynamic indices are treated as potentially equal unless the compiler can prove otherwise.
const bindings do not provide mutable places. Place parameter types are invariant so a callee cannot reinterpret the caller's storage through a broader type.
Escaping Places
Provisional design: Nonescaping calls and projected routes have settled behavior. Storing or returning a place requires lifetime, ownership, and concurrency rules that are still being designed.
Function handles use the same @ root-and-route idea; see Function Values and Composition. The Reference contains the exact value and aliasing rules.
Strings and Graphemes
Dewy strings are immutable sequences of Unicode extended grapheme clusters: the units people usually perceive as characters.
let text = "café 👨👩👧👦 🇺🇸"
text.length
text[5]
Indexing and iteration do not split an accent from its letter or a joined emoji sequence into unrelated pieces. grapheme is a string of length one; char is an alias for the same type.
Single and double quotes have the same string semantics.
Interpolation
Braces insert an expression into a string. Adjacent fields produce one string:
let unread = 3
printl"You have {unread} unread messages."
let combined = "{greeting}{name}"
Interpolation uses the same conversion as value as string. A type can therefore participate through the general conversion protocol rather than needing a special interpolation-only method.
An implementation may stream literal chunks and converted values directly into print or printl. When the expression itself must survive as a string value, it materializes an equivalent immutable string. That representation difference is not visible to the program.
Joining Strings
+ does not concatenate. Two strings combine by interpolation, and any number of them by join on an array of strings:
let main = ():>int64 => {
let words:array<string> = ["one" "two" "three"]
printl(words.join", ") # one, two, three
let pieces:array<string> = [] # the string builder is an array
let i:int64 = 0
loop i <? 3 {
pieces.push"{i * i}"
i += 1
}
printl(pieces.join"-") # 0-1-4
return 0
}
join without a separator concatenates directly. It never mutates the array, so it works on any array of strings — including exact-length ones — and the result can be returned or stored like any other string.
Bytes that should be text are decoded with a check: bytes as string | none gives the string when the bytes are valid UTF-8 and none otherwise, so invalid input is a case to handle rather than an exception.
Iterating Text
Iteration yields graphemes:
let text = "café 👨👩👧👦 🍀"
loop index in 0.. and character in text
if character not =? ' '
printl"{index}: {character}"
Character ranges use scalar order when every anchor is a one-scalar grapheme:
loop letter in 'a'..'z'
print(letter)
Natural-language collation and enumeration of arbitrary multi-scalar graphemes require explicit APIs rather than an invented universal ordering.
Indexing by Position
Iterating covers most text handling, but a tokenizer wants to look at positions: text[i] for the grapheme at i, text[a..b] for a slice. Dewy proves those in bounds instead of checking at runtime, and for a string whose length is only known at runtime the proof comes from what your code already says — a loop or guard on i <? text.length is enough:
let first_word = (text:string):>string => {
let i:int64 = 0
loop i <? text.length {
if text[i] =? " " { return text[0..i) }
i += 1
}
return text
}
printl(first_word("héllo world")) # héllo
Without such a guard, text[i] is a compile error ("string index is not proven in bounds") rather than a possible crash.
Slicing
Range indexes select immutable grapheme slices:
let text = "this is some text"
let prefix = text[..4]
let middle = text[5..11]
let suffix = text[13..]
end refers to the final grapheme index — it is text.length - 1, so text[end - 1] and text[2..end] work too, on strings of any length as long as the length is proven large enough. Open and closed range boundaries retain their ordinary meanings.
Representation Views
Use explicit conversions when code needs a lower-level representation:
let bytes:array<uint8> = text as array<uint8>
let scalars:array<uint32> = text as array<uint32>
let clusters:array<grapheme> = text as array<grapheme>
The byte view is UTF-8. The scalar view contains valid Unicode scalar values. Converting grapheme pieces back to a string concatenates and segments them again, so adjacent pieces may combine into a new grapheme.
Strings preserve the exact scalar spelling supplied by the program. Equality does not silently normalize canonically equivalent text; normalization-aware comparison belongs to an explicit text API.
See the Reference for exact string semantics.
Containers
Square brackets collect values. Whitespace separates elements, so ordinary Dewy containers do not require commas.
Arrays
An array is an ordered homogeneous value:
let names = ["Ada" "Grace" "Linus"]
printl(names[1])
Arrays are indexed from zero. An annotation can state the element type and a known length:
let names:array<string> = ["Ada" "Grace"]
let triple:array<int64 length=3> = [10 20 30]
triple.length
triple[end]
triple[0..1]
Array values copy by meaning. If a function should deliberately update an existing array or element, pass its place:
fill(@names)
set(@triple[1])
Growing Arrays
An array declared without an exact length can change length through methods on the value itself:
let xs:array<int64> = [10 20]
xs.push(30) # [10 20 30]
xs.insert(15 1) # [10 15 20 30]
let last = xs.pop # 30
let first = xs.pop(0) # 10
xs.truncate(1) # [15]
xs.sort
xs.clear
push, pop, insert, truncate, clear, reserve, and sort are the growth methods; pop yields the removed element. Container operations always live on the container: there is no free push(xs x).
Operations that could fail must be proven safe at compile time. xs.pop needs a proven non-empty array, xs.pop(i) and xs.insert(v i) need a proven index, and an ordinary xs[i] needs a proven bound. Literal lengths, push/pop stepping those lengths, and guards such as if i <? xs.length all supply the proof; see Refinements.
Sorting
xs.sort orders fixed-width integer elements ascending. Any other elements are sorted by a key: a function of one element that returns a fixed-width integer. reverse=true sorts descending. Both directions are stable, so elements with equal keys keep their original order:
let Hit = type of any & [length:uint64 name:string]
let hits:array<Hit> = [Hit[length=3 name="a"] Hit[length=10 name="b"] Hit[length=5 name="c"]]
hits.sort(key=(h) => h.length reverse=true) # b, c, a: the longest match first
let words:array<string> = ["ccc" "a" "bb"]
words.sort(key=(w) => w.length) # a, bb, ccc
let ns:array<int64> = [3 (-1) 2]
ns.sort(reverse=true) # 3, 2, -1
The key's parameter needs no annotation: it takes the element type from the call (see Function Values). A named function is passed with @: hits.sort(key=@by_length).
Loop Capture
A loop can express values for [] to collect:
let squares = [
loop number in 1..10
number^2
]
This is the ordinary loop expression, not a separate comprehension grammar.
Spreading
A trailing ... inserts the contents of an existing container into a surrounding literal. Arrays (and sets) spread their elements into an array literal, mixed freely with written elements; objects spread their fields into an object literal, where a later entry with the same name wins — the natural "copy with changes" form:
let main = ():>int64 => {
let heads = [1 2]
let tails = [8 9]
let both = [heads... tails...] # [1 2 8 9]
let padded = [0 both... 10] # [0 1 2 8 9 10]
let point = [x=1 y=2]
let moved = [point... x=5] # [x=5 y=2]
let tagged = [point... label="origin"] # [x=1 y=2 label="origin"]
return padded.length + moved.x + tagged.y # 13
}
The result's length is known exactly when every spread operand's length is; otherwise it is a runtime-length array. Spreading into dictionary and set literals is not implemented yet.
Shapes and Multidimensional Data
Arrays are also the foundation for vectors, matrices, and tensors. Dewy's shape and literal syntax must support contiguous multidimensional representations without preventing ordinary arrays of arrays.
Provisional design: Exact multidimensional shape annotations, dimension separators, broadcasting, and axis selection are still being unified. The one-dimensional
array<T length=N>form and nested array values are settled. Multidimensional arrays will likely look likearray<T length=[l1 l2 ... lN]>, and make use of;and newlines for tracking new dimensions in array literals (tbd how it interplays with loop capture)
Dictionaries and Bidictionaries
A dictionary collects key/value pairs written with ->:
let ratings = [
"star trek" -> 89
"star wars" -> 73
]
Dictionaries retain insertion order. Iteration yields key/value pairs in that order:
let ratings = [
"star trek" -> 89
"star wars" -> 73
]
loop [title score] in ratings
printl"{title}: {score}"
Looking Up Keys
Indexing a dictionary is only allowed when the compiler can prove the key is present. A key is proven when it came from the literal, was just stored, is being iterated, or was tested with in?:
let ratings = ["star trek" -> 89 "star wars" -> 73]
let trek = ratings["star trek"] # from the literal
ratings["dune"] = 91
let dune = ratings["dune"] # just stored
let title = "alien"
if title in? ratings
printl"{ratings[title]}" # proven by the guard; the guard's search is reused
This is Dewy's general rule for operations that raise exceptions in Python: they must be proven safe or they do not compile. When a key may be missing, say so with get:
let ratings = ["star trek" -> 89 "star wars" -> 73]
let maybe = ratings.get("alien") # int64 | none
let score = ratings.get("alien" 0) # 0 when absent
Changing a Dictionary
d[key] = value stores a value, replacing the value of an existing key in place or appending a new entry at the end. pop removes a proven key and yields its value; with the name-only default argument the key need not be proven:
let ratings = ["star trek" -> 89 "dune" -> 91]
let removed = ratings.pop("dune") # proven present
let gone = ratings.pop("alien" default=(-1)) # -1 when absent
ratings.clear
d.length counts entries, d.keys is a set of the keys, d.values an array of the values in insertion order, and d1 | d2 (or d1 or d2) merges two dictionaries the way Python does: shared keys take the right value while keeping the left position, and new keys append. A dictionary must not change while a loop iterates it; the compiler rejects stores, pop, and clear inside such a loop.
Dictionaries are ordinary values: they can be passed to functions, returned, stored in objects, and written as literals in any expression. A callee that stores into a dictionary parameter works on its own copy unless the parameter is a place.
<-> describes a bidirectional mapping whose values can be looked up from either side.
Sets
A set holds each member once and remembers first-seen order:
let permissions = set["read" "write" "read"] # two members
permissions.add("execute")
"read" in? permissions
permissions.length
let taken = permissions.pop("read") # proven: it came from the literal
permissions.pop("nope" default=none); # absent: nothing happens
pop follows the dictionary rule: a proven member, or a default when it may be missing. s.values is an array of the members, and the set operators produce new sets:
let evens = set[0 2 4 6]
let small = set[0 1 2 3]
let both = evens & small # intersection: 0 2 (`and` also works)
let either = evens | small # union: 0 2 4 6 1 3 (`or` also works)
let only = evens - small # difference: 4 6
let odd_one_out = evens xor small
Because d.keys is a set, the same operators compare the keys of two dictionaries.
Provisional design: Bidirectional dictionaries, equality and ordering of containers, compound operator forms such as
|=, and keys beyond words and strings remain under design.
Objects also use square brackets, but named fields with = distinguish them from positional containers. Continue with Structural Objects, or consult the exact array and container reference.
Structural Objects
An object is a value with named fields. It does not require a separate class declaration:
let account = [
name = "Ada"
active = true
]
account.name
Field names, field types, and their order form the object's structural type.
Naming an Object Shape
A type alias gives a structural shape a reusable name:
let Pair:type = [left:int64 right:int64]
let origin:Pair = [left=0 right=0]
The alias does not create a runtime class object or nominal identity. Another value with the same required structure satisfies the same structural contract.
Recursive Shapes
A named shape can refer to itself, as long as the recursion goes through a union — usually | none, so that a chain can end:
let Node:type = [value:int64 next:Node|none]
let sum = (list:Node|none):>int64 => {
let total:int64 = 0
let cur:Node|none = list
loop cur is? Node {
total += cur.value
cur = cur.next
}
return total
}
let main = ():>int64 => {
let list:Node|none = none
let i:int64 = 1
loop i <=? 4 {
list = [value=i next=list]
i += 1
}
return sum(list) # 10
}
cur is? Node narrows cur for the loop body, and cur.next is? Node would narrow the field itself. A field typed plainly Node is rejected: without a union there is no last node. Recursive values are still values — assigning a chain to another binding copies the whole chain.
Combining Object Requirements
& combines structural types without creating nominal identity:
const Located:type = [line:int64 column:int64]
const Labeled:type = [label:string]
const LabeledLocation:type = Located & Labeled
Fields present on only one side are retained. When both sides contain the same field, its required type is the intersection of the two field types. If that becomes never, the complete object type is impossible. The two declarations must also agree about whether the field is mutable; silently choosing one would break the other contract.
Constructing Objects
A named object type is its own constructor: call it with the fields in order, or by name, and leave out any field the type gives a default for.
let Span:type = [start:int64 stop:int64 = start label:string = "span"]
let a = Span(1 9)
let b = Span(stop=5 start=2 label="b")
let c = Span(7) # stop defaults to start
printl"{a.stop - a.start} {b.label} {c.stop}" # 8 b 7
The field list is the signature — the same rules as a function's parameters, with defaults allowed to use earlier fields — so there is no separate class declaration to write. A constructor can still be an ordinary function returning an object when construction needs more than filling fields:
let make_pair = (left:int64 right:int64):>Pair =>
[left=left right=right]
let pair = make_pair(20 22)
A type with a structural body can construct that body directly:
let unit_x = Pair[left=1 right=0]
The object literal is checked against the named structure. When a type also carries nominal ancestry, the constructed value retains that identity; Defining Exceptions shows such a hybrid type.
Methods
A named type can carry behavior: method rows next to the fields, whose bodies use the fields by name. A method that changes fields needs a binding to work on; one that only reads can be called on anything.
let Span:type = [
start:int64
stop:int64 = start
width = () => stop - start
grow = (by:int64) => { stop += by }
]
let s = Span(3 7)
s.grow(2)
printl"{s.start}..{s.stop} is {s.width} wide" # 3..9 is 6 wide
When construction itself needs logic, add a constructor overload with &= — an ordinary function returning the type — and Span(…) picks the field-wise constructor or the overload by the arguments:
Span &= (text:string):>Span => Span(0 text.length)
let whole = Span("seven..") # 0..7
Behavior Inside Objects
Function fields can use sibling fields directly:
let counter = (start:int64=0) => [
value = start
increment = () => (value += 1)
]
let count = counter(40)
count.increment
count.increment
printl"count is {count.value}"
Accessing a zero-argument function field calls it when that call is valid. Explicit count.increment() is equivalent.
Objects Are Values
Ordinary copies are independent:
let Document:type = [name:string saved:bool]
let original:Document = [name="draft" saved=false]
let copy = original
copy.saved = true
# original.saved is still false
To update the caller's object deliberately, accept and pass a place:
let save = (@document:Document):>void => (document.saved = true)
save(@original)
Fields and array elements can be selected directly, such as set(@original.saved).
Operators and Conversions
Objects participate in operators and conversions through typed overloads. The precise overloadable conversion protocol is preferred over a second class-specific “dunder” model:
let __add__ = __add__ & (
(a:Pair b:Pair):>Pair =>
[left=a.left + b.left right=a.right + b.right]
)
Provisional design: Extracted methods, escaping captured fields, function-handle identity, and the final convention for attaching overloads to structural types are part of the function-handle and generic-object design.
The Reference defines structural object behavior and value semantics.
Numbers and Bases
An integer literal may use a radix prefix. Dewy supports integer numerals through base 16:
| Base | Prefix | Digits |
|---|---|---|
| 2 | 0b | 0–1 |
| 3 | 0t | 0–2 |
| 4 | 0q | 0–3 |
| 6 | 0s | 0–5 |
| 8 | 0o | 0–7 |
| 10 | 0d | 0–9 |
| 12 | 0z | 0–9, x, e |
| 16 | 0x | 0–9, a–f |
Alphabetic digits are case-insensitive in these integer forms. Decimal is the default, so 42 and 0d42 denote the same value.
0b10101010 # 170
0t121010 # 435
0q123 # 27
0s1432 # 380
0o1234567 # 342391
0xdeadbeef # 3735928559
Underscores may group integer digits without changing the value:
let population = 1_000_000
let mask = 0b1111_0000
Packed Based Strings
A radix prefix followed by a quoted digit sequence produces exact packed data rather than an integer:
const program:array<uint8> = 0q"000000010002"
const header:array<uint8> = 0x"deadbeef"
Power-of-two bases have a compositional bit width and can therefore be packed directly:
| Base | Prefix | Bits per digit |
|---|---|---|
| 2 | 0b | 1 |
| 4 | 0q | 2 |
| 8 | 0o | 3 |
| 16 | 0x | 4 |
| 32 | 0u | 5 |
| 64 | 0g | 6 |
Digits contribute bits from left to right, most-significant bit first. A final partial byte is padded with zero bits on the right. Whitespace and comments may separate digits.
Base 64 uses + or - for digit 62 and / or _ for digit 63. Trailing = characters are accepted as explicit padding and do not contribute bits. Unlike numeric underscores, _ inside a base-64 string is a digit.
Based strings for non-power-of-two bases are reserved until their sequence-width and composition rules are settled. The Reference gives the exact literal and packing rules.
Operators
Dewy operators are typed operations. The same spelling may select different overloads when the operand types give it a coherent meaning.
Arithmetic
left + right
left - right
left * right
left / right
left // right
left % right
base ^ exponent
Prefix + and - express sign. Prefix /value is reciprocal. Composite operator chains such as value^/2 retain the first operator's precedence and can express roots compactly.
/ on integers produces an exact rational (1/3), while // is floor division. ^ raises integers and rationals to integer powers; a negative constant exponent makes the result rational. On sets, |/or, &/and, -, and xor are union, intersection, difference, and symmetric difference, and | also merges dictionaries.
Comparison and Tests
Dewy distinguishes tests from assignment:
left =? right
left not =? right
left <? right
left <=? right
value in? range
value is? Type
value isnt? none
Boolean and Bitwise Operations
The English operators and, or, not, nand, nor, xor, and xnor express Boolean composition and short-circuit where their truth rule permits it.
&, |, and ~ are the same operations as and, or, and not at a tighter precedence — above the comparisons, where the words sit below them. Use the symbols to compose things that are then compared or tested as a whole: types (x is? int64|string, d:int64 & ~0), overload sets (@f & @g), sets, and masks (flags & MASK =? 0). Use the words for logic over comparisons (x >? 0 and y <? n); x >? 0 & y >? 0 would parse as x >? (0 & y) >? 0.
Juxtaposition
Adjacent expressions reuse one syntactic relationship:
function(argument) # call
values[index] # index
2distance # multiplication
values... # spread into a surrounding collector
The parser keeps meaningful alternatives until types and context select the operation. This is why function calls, indexing, and mathematical notation can share a consistent surface form without textual heuristics.
Pipes and Conversion
value |> @transform
@transform <| value
value as Destination
value transmute Representation
as performs a semantic conversion. transmute reinterprets a compatible representation and is not a substitute for conversion.
Assignment
= updates a mutable binding or selected place. Most operations have a combined-assignment form:
count += 1
flags xor= mask
Combined assignment has assignment precedence. When its right side is itself an assignment-like expression, grouping is required—for example, () => (value += 1).
An attached postfix ; suppresses an expression's produced value.
Place and Function Selection
A leading @ selects the place at the end of a complete field-and-index route, or selects a function binding as a handle. It appears only at the beginning of that route:
@value
@pair.left
@items[index]
There is no operator asking whether two places are the same place: places are borrows, not values, and independent values never share observable storage (the once-reserved @? was retired for that reason).
For functions, an ungrouped @ chain selects and partially evaluates without calling. Grouping ends that chain, so (@worker.callback)(5) calls the selected function. Function Values and Composition develops the complete rule after introducing places and objects.
Elementwise and Vectorized Operations
Provisional design: A leading
.on an operator applies it elementwise, whilef.(values)vectorizes a function call. Broadcasting and multidimensional shape rules must be specified together before edge cases are normative.
The complete and canonical precedence table lives in the Reference. Use () or {} when the intended grouping is not represented directly by that table.
Function Values and Composition
Dewy does not require a separate “functional mode.” Functions, loops, blocks, and ordinary values compose using the same expression rules as the rest of the language.
Selecting and Passing Functions
A function contract can appear anywhere another type can:
let apply = (
transform:<(value:int64):>int64>
value:int64
):>int64 => transform(value)
let square = (value:int64) => value^2
apply(@square 5)
@square selects the function binding rather than calling square with no arguments.
A function literal written where a function type is expected can leave its parameters unannotated: each takes the type of the matching parameter of the expected function type (positional parameters by position, keyword-only ones by name), and the result type is inferred from the body as usual. An annotated parameter keeps its annotation.
let apply = (
transform:<(value:int64):>int64>
value:int64
):>int64 => transform(value)
apply((value) => value * 2 5) # `value` is an int64 here
A leading @ governs the complete ungrouped selector-and-application chain. Function-valued nodes inside that chain are selected rather than called. Grouping ends the chain, so a following argument group performs an ordinary call:
@worker.callback.metadata # metadata on the callback function value
worker.callback(5).status # call callback normally, then read its result
(@worker.callback)(5).status # select callback explicitly, then call it
The parentheses around @worker.callback terminate the selection chain. This uses ordinary grouping rather than a separate “call this handle” operator.
Partial Operators
A binary operator applied to only its right operand, in parentheses, is a partial operator: a function of the missing left operand — the shorthand for the one-line lambdas that facts and sort keys keep asking for:
let main = ():>int64 => {
let names:array<string> = ["bb" "a" "ccc"]
names.sort(key=(.length)) # (s) => s.length
let doubled:<(v:int64):>int64> = (* 2)
let text = "hello"
let k:uint64<(<? text.length)> = 3 # uint64<i => i <? text.length>
let digit:uint64<(in? 0..9)> = 7
return 0
}
Only for operators that have no prefix form ((- 1) is still negative one): the comparisons and tests, ., as, transmute, *, /, //, ^, %, \. A partial operator is typed exactly like an unannotated lambda — from the function type it is used against.
Partial Evaluation
A function handle can bind some arguments now and leave the rest open:
let add = (left:int64 right:int64) => left + right
let add5 = @add(5)
add5(24) # 29
Explicit arguments are captured when the partial function is created. Default expressions remain per-call fallbacks.
Every argument group still inside an unbroken @ chain performs another partial-evaluation step. An argument group outside a grouping boundary calls:
@add(1)(2) # save 1, then save 2; still a function
(@add(1))(2) # save 1, then call with 2
@add(1)() # empty second partial evaluation; still a function
(@add(1))() # call the partially evaluated function
An empty partial evaluation neither invokes the function nor evaluates defaults.
A function member can be selected and partially evaluated at the end of a stable object route:
let on_item = @worker.callback(5)
This preserves the function field's receiver. If another call produces the object, bind that result first because a temporary call result is not a place-route root:
let worker = make_worker()
let on_item = @worker.callback(5)
@make_worker() is an empty partial evaluation of make_worker, not a call.
Transforming and Selecting Values
A loop already expresses the operations often called map and filter:
let values = [1 2 3 4 5 6]
let squares = [
loop value in values
value^2
]
let odd = [
loop value in values
if value % 2 =? 1
value
]
The array collects what the loop expresses. No separate comprehension or callback vocabulary is required for these direct cases.
Reusable library functions can be built from the same pattern once generic function contracts are available:
let map = <T U>(
transform:<(value:T):>U>
values:array<T>
):>array<U> => [
loop value in values
transform(value)
]
Generic functions work today for the direct cases: declare the type parameters in <…> before the parameter list, give the result a type, and call the function by name — the compiler infers the type arguments from the call and compiles one instance per distinct binding:
let first = <T>(xs:array<T>):>T | none =>
if xs.length >? 0 xs[0] else none
let main = ():>int64 => {
let nums:array<int64> = [7 8 9]
let n = first(nums) # first<int64>
if n is? int64 { return n }
return 0
}
T of int bounds a parameter to a family of types; inside the body, the operations available are those of the concrete types the call supplied, checked at that call. A generic function cannot yet be passed as a value or declared inside another function.
Capturing an Enclosing Scope
A nested function can use names from its lexical environment:
let counter = (start:int64=0) => [
value = start
increment = () => (value += 1)
]
A local function may read the locals and parameters of the functions around it, and it sees them as they are when it is called:
let main = ():>int64 => {
let base:int64 = 10
let scale = (v:int64):>int64 => v * base
let a = scale(2) # 20
base = 100
let b = scale(2) # 200: the current value of `base`
return a + b
}
In the current compiler such a function is lambda-lifted: the values it reads become hidden trailing parameters, passed at every direct call. Two things follow from that and are rejected for now: a local function cannot assign to a captured variable (keep shared mutable state in an object, or return the new value), and a capturing function cannot be used as a value — stored, passed to another function, or returned — because that needs a closure record, which is not implemented yet. Non-capturing functions are unrestricted as values.
Provisional design: The lexical meaning of captures is settled. Escaping closure storage, handle identity, explicit function copying, and general user-written generics remain under design and implementation.
See Functions and Calls for ordinary argument behavior and the Reference for exact function-handle rules.
Refinements and Proven Facts
A refinement is a type together with facts its values must satisfy. Length is a familiar example:
let triple:array<int64 length=3> = [10 20 30]
The type says more than “array of integers”; it also says that the valid shape has exactly three elements.
Why Refinements Matter
Useful facts let Dewy reject invalid programs and remove unnecessary runtime work:
let first = triple[0]
The index needs no dynamic bounds check because the type already proves it valid.
The same idea can describe nonempty containers, positive values, relationships between parameters and results, and state changes such as an operation reducing a collection's length by one.
Writing Refinements
A parameterize block after a type may hold conditions. A one-argument lambda states a condition on the value itself; a ?-comparison on length states one on a container:
Positive = int< i=>i>?0 >
NonEmptyArray = array< length>?0 >
score:Positive = 42
values:NonEmptyArray<int> = [3 5 8]
first = values[0]
Conditions and parameters are told apart by their shape: a lambda, a ?-comparison, or a length=N assignment is a condition; anything else is a parameter. NonEmptyArray leaves the element type open, so NonEmptyArray<int> supplies it later.
Checking a value against a refined type has three outcomes: proven (a literal or known fact establishes the condition, with no runtime cost), refuted (score:Positive = -3 is an error), and unknown, which is reported as unproven rather than false. The binding then carries the base type plus the proven facts, so values[0] needs no runtime check.
Each condition is a fact, and a block of facts is a type of its own that & combines with any type: int & <i => i >? 0> is Positive again, and (int64 | uint64) & <i => i >? 0> refines both members. That is how a function states what it establishes about its inputs. A boolean result says it per arm — startswith is declared (text:string prefix:string):> true & <prefix.length <=? text.length> | false — and a proposition as the result type is a type predicate, true when it holds and false when it doesn't:
let Token:type = $abstract type of any & [text:string]
let Word = type of Token & []
let is_word = (tok:Token) => tok is? Word # inferred: `:> tok is? Word`
let name_of = (tok:Token):>string => {
if is_word(tok) { let w:Word = tok return w.text } # `tok` is a Word here
return "?"
}
let skip_marker = (src:string i:uint64):>uint64<n => n <=? src.length> | none => {
if i >=? src.length return none
if src[i..].startswith("[[") { return i + 2 } # the fact keeps `i + 2` within `src`
return i
}
The function proves its facts at every return (return tok is? Word is its own proof; return true needs tok narrowed to Word at that point), and every caller gets them where the result is known.
Facts from Ordinary Control Flow
Dewy should infer common refinements from the code programmers already write:
if index >=? 0 and index <? values.length
use(values[index])
Inside the body, the condition establishes the indexing precondition. Assignment or a call that may mutate a relevant value invalidates facts that are no longer guaranteed.
Assertions
Sometimes the fact you rely on is not one the compiler would state on its own. $assert states it and asks the compiler to prove it — proven assertions cost nothing, refuted ones are errors, and an assertion the compiler cannot decide is reported as unproven rather than silently trusted:
let xs:array<int64> = [1 2 3]
$assert xs.length =? 3, "three elements"
let get = (ys:array<int64> i:int64):>int64 => {
$runtime_assert i >=? 0 and i <? ys.length, "index {i} out of range"
return ys[i]
}
let main = ():>int64 => get(xs 1) # 2
$runtime_assert checks at runtime instead. Its failure path leaves the program with a report on stderr laid out like a compiler error — the line with the condition underlined, the message under it, and notes with the values that went into it — and exit status 101, so after it the compiler knows the condition held: ys[i] above needs no further proof, just as it would not after if i <? 0 or i >=? ys.length { return 0 }.
Explicit Boundaries
The intended model distinguishes several outcomes:
- a fact the compiler proves automatically has no runtime cost;
- an explicit runtime check refines the value after it succeeds;
- a checked proof can discharge an obligation outside automatic inference;
unsafecan assert an unproved obligation while making that trust boundary visible for review.
A refinement on a parameter is a contract: every call has to prove it, and the body gets to assume it. Since the parameter has a name, the condition just uses it (whole >? 0); the i => … lambda form is for type aliases, where there is no name yet. Guards are the usual proof:
let percent = (part:int64 whole:int64<whole >? 0>):>int64 => part * 100 // whole
let share = (part:int64 whole:int64):>int64 => {
if whole >? 0 { return percent(part whole) }
return 0
}
printl"{share(3 4)}%" # 75%
Without the guard, percent(part whole) is an error — "cannot prove refinement" — and so is dividing by whole directly, since Dewy proves every division's divisor nonzero instead of letting it crash.
The same contract works the other way round on results — (n:int64):>int64<i => i >=? 1> promises every caller a positive number, and every return inside has to prove it — and on fields: let Ratio:type = [top:int64 bottom:int64<bottom >? 0>] is checked wherever a Ratio is built or bottom is stored, and assumed wherever bottom is read, so r.top // r.bottom never needs a guard. Dewy's own Rational is declared exactly like that.
Provisional design: Refined annotations on bindings, parameters, results, and fields, integer comparisons against constants, length facts, and interval reasoning are settled. Richer propositions, checked proof values, and the
unsafesyntax are not fully specified. Unsupported general Dewy expressions must not silently become refinement claims.
The design goal is inference-first: ordinary code should expose enough facts for routine safety without requiring programmers to write proofs throughout application code.
Physical Quantities and Units
Dewy places physical dimensions in the type system. A length is not interchangeable with a duration merely because both happen to use the same machine number.
let distance = 120m
let elapsed = 10s
let speed = distance / elapsed
Adding incompatible dimensions is an error:
2kg + 3m
Units Are Ordinary Values and Types
Writing a number next to a unit multiplies them. Group compound units when that makes the intended precedence clearer:
let acceleration = 9.8(m/s^2)
let force = 5kg * acceleration
Unit scales can fold at compile time. The unit portion may disappear entirely from the runtime representation once it has guaranteed that operations are dimensionally valid.
The base dimensions are Time, Length, Mass, Current, Temperature, Amount, Luminosity, and Angle. Multiplying, dividing, and raising quantities combines their dimensions; adding, subtracting, and comparing require the same dimension.
Canonical Scales
Every dimension has a canonical unit — the SI base unit, and the whole turn for angles — and every other unit is an exact rational scale of it. 1/2 * mass * velocity^2, 30m/s, and 9.8(m/s^2) therefore fold to exact constants, and a quantity that survives to runtime carries only its number in the canonical scale:
import units # the units beyond time; the second and its scales are always in scope
const mass = 10kg
const velocity = 30m/s
let energy = 1/2 * mass * velocity^2 # 4500 J
let joules:rational = energy / J # dividing by a unit yields the count
(const keeps the quantities compile-time so the arithmetic folds exactly. A rational that survives to runtime has big-integer parts, so its arithmetic never overflows; rational<int64> is the explicit word-sized form, whose runtime arithmetic yields rational<int64> | Overflow for the caller to handle.)
Angles use the turn so that degrees are exact (45° is 1/8 turn) and trigonometry reduces exactly before computing. cos, sin, and tan accept an angle and return a fixed-point value:
from units import (N m cos °)
let work = 20N * 10m * cos(45°) # about 141.42 J
Time is the one dimension the prelude carries — s, ms, us, ns, minute, hour, and Duration are always in scope. Every other unit is imported from the library module units (import units for all of them, from units import (m kg) for a selection): the SI base units, the usual prefixes (km, cm, mm, g, mg), the derived units Hz N Pa J W, turn, °/deg, rad, and the trigonometric functions over angles.
Representation-Parameterized Quantities
A duration is a numeric representation multiplied by the Time dimension:
const Duration:type = <T of real>(T * Time)
Duration<int64> preserves the selected integer representation. The Time portion supplies meaning and static checking without requiring a wrapper object around the integer. sleep takes a time quantity — sleep(300ms) — and converts to whole nanoseconds at the system boundary; a dimensionless number is rejected.
Converting Scales
Units of the same dimension represent the same kind of physical value at different scales. Dividing by a unit yields the count in that unit, as in distance / km. Mixed-unit arithmetic first establishes compatible dimensions and then applies the exact scales.
Unit Libraries
The standard library should organize unit catalogs by domain so programs import useful names without making every abbreviation globally ambiguous. SI, information, customary, astronomical, and domain-specific units can build on the same dimension model.
Provisional design: The base-dimension algebra, canonical scales, the rational/fixed representations, and dimension erasure are settled. Still under design: printing a quantity in the unit it was written in (and
x as kmto choose one), declaring new base dimensions in library code, offset units such as Celsius (points versus deltas), calendar-relative durations, and catalog organization.
See the exact physical quantity reference.
Effects
An effect describes an observable interaction a function may perform beyond producing its return value. Effects let callers and the compiler reason about mutation, blocking, I/O, allocation, failure, and other behavior relevant to composition.
Why Effects Belong in Contracts
Knowing that a function only reads a value allows the compiler to preserve refinements and borrow storage invisibly. Knowing that it may mutate, block, or escape a value changes what remains safe afterward.
Effects are therefore not only documentation. They participate in call checking, optimization, lifetime reasoning, and the construction of restricted execution environments.
Errors Are Return Values
An expected failure is not represented by putting an error name in the effect set. It is an ordinary alternative in the return type:
let load = (id:RecordId)
:> (Record | NotFoundError | DatabaseError) & reads<database>
The union says which value the caller receives. reads<database> says what evaluating the function may do. Keeping those two ideas separate lets callers recover from a returned error without pretending that the database access itself did not happen. See Errors as Values.
noreturn
noreturn is the settled semantic effect of a function that does not return to its caller. The result of calling such a function has type never:
let die = (message:string):>never => {
printl(message)
exit(1)
}
The two ideas remain distinct: noreturn describes what the call does, while never is the type of the path after that call. The spelling for declaring noreturn in a general effect contract has not been selected, so this book does not place it in the return-type position.
General Effect Design
Provisional design: The full effect vocabulary and syntax are not yet fixed. It must support inferred ordinary code, explicit public contracts, transitive effects through calls, effect-polymorphic helpers, and deliberate handling or masking at a clear boundary.
The design should keep common programs uncluttered: most local effects should be inferred, while APIs state the effects that matter to their callers.
Modules, Imports, and the Prelude
Every source file is a module. Imports bring typed top-level bindings from another module into the current one.
Importing Names
Paths resolve relative to the importing file:
from p"helpers.dewy" import format_name
import format_name from p"helpers.dewy"
The order may be written either way. Import several names with a comma sequence or a parenthesized whitespace sequence:
from p"helpers.dewy" import parse, validate, save
from p"helpers.dewy" import (parse validate save)
Rename a binding with as:
from p"helpers.dewy" import (save as save_document)
Namespaces and Splats
Bind a module namespace when several uses should remain qualified:
import p"helpers.dewy" as helpers
let result = helpers.parse(input)
let item:helpers.Item = result
Importing only the path splats its top-level bindings into the current scope:
import p"helpers.dewy"
Name collisions and import cycles are compile errors.
Paths Are Compile-Time Values
p is an ordinary prelude function constructing a structural path value:
from [path="helpers.dewy"] import parse
The compiler must know the exact path while building the module graph. A runtime-computed string cannot choose a source import.
File suffixes are conventional; a file containing Dewy source does not acquire different language semantics because of its extension.
Initialization
Reachable modules initialize once in dependency order. Their top-level expressions run before the entry module proceeds to main.
Targets
$target is the compile-time name of the backend being compiled for (x86_64, riscv, arm, c, wasm32), the same names udewy uses. Comparing it selects code during checking, so an arm for another target is skipped entirely and may import files that exist only there:
if $target in? ["x86_64" "riscv" "arm" "c"] {
from p"linux/io.dewy" import (_write_stdout _write_stderr)
}
if $target not =? "wasm32" { import p"native_only.dewy" }
$supported_targets = ["x86_64" "c"] rejects compilation for any other target. Only comparisons of $target fold this way; an ordinary if true still checks every arm.
The Source Prelude
Ordinary modules receive a small set of default imports: path construction, printing, rationals and fixed-point numbers, units, and host facilities such as sleep where the target supplies them. The prelude's portable files import their target-specific primitives with the same $target gating.
A module can opt out:
$no_prelude = true
That choice applies to the containing module and does not silently change modules it imports.
Provisional design: Installed package lookup, non-source artifacts, and the naming policy for domain libraries are still evolving. File-relative source imports and the per-module prelude rule are settled.
The Reference defines module resolution and initialization.
The Standard Library
Dewy's standard library should make common programs convenient without turning basic language behavior into hidden magic.
The language defines constructs such as arrays, objects, functions, and ranges. The library builds reusable policies and services on top: paths, files, text processing, networking, collections, clocks, concurrency, parsing, and platform capabilities.
The Prelude
A small source prelude provides names that ordinary programs use constantly:
Pathandpfor paths;printandprintlfor basic output;Durationand common exact time scales;- target-provided essentials such as
sleep.
Prelude names are ordinary bindings and may be shadowed. $no_prelude = true requests a module without implicit prelude imports.
Library Design Principles
- Common operations should have straightforward defaults.
- Platform capabilities and observable effects should appear in types or effect contracts where they matter.
- Domain libraries should compose with the same arrays, objects, iterators, units, and errors used elsewhere.
- Zero-cost abstractions should remain possible without making low-level representation the default user interface.
- Portable interfaces should distinguish language guarantees from target-specific availability.
The standard library is still being built. Future library explorations are maintained outside the reading path until they have real APIs and programs behind them; the current queue is preserved in the repository's site/DOCUMENTATION_PROJECTS.md.
Language Feature Index
This page is a topical index. The main book teaches the same material in a progressive order.
Program Structure
- Source execution and
main - Bindings and lexical scope
- Modules, imports, and the source prelude
- The standard library
Expressions and Control Flow
- Expressions, produced values, suppression, and blocks
- Conditionals and flow chains
- Optional values and type narrowing
- Errors and exception values, safe navigation, and propagation
- Loops, iterator conditions, and multiiterators
- Ranges, bounds, membership, and slicing
Functions
- Function literals, contracts, calls, defaults, and argument kinds
- Static overloads
- Function values and composition
- Effects
Values and Data
- Value semantics and explicit places
- Strings, graphemes, interpolation, and representation views
- Arrays and other containers
- Structural objects and constructors
Types and Operations
- Type inference, numeric types, unions, and aliases
- Numeric bases and packed based strings
- Operators, juxtaposition, conversion, and precedence
- Refinement types
- Physical dimensions and units
Features with provisional design or incomplete compiler support are catalogued separately in Language Design and Compiler Support, so implementation status does not obscure this index.
Language Design and Compiler Support
The main chapters describe the intended Dewy language. This appendix explains how to interpret features whose design or implementation is still moving.
Two Separate Questions
A feature can have settled language semantics before the compiler implements it. Conversely, an experimental implementation can exist while some edge cases remain open. Documentation therefore tracks two independent kinds of maturity:
- Design maturity: settled, provisional, or open.
- Implementation maturity: implemented, partial, or not yet implemented.
Settled design remains part of the normal Learn and Reference prose. Provisional sections state the boundary of what has been decided. Open questions live here or in the project's design notes rather than being presented as established syntax.
Current Compiler Snapshot
The hosted compiler currently covers a substantial core: bindings and scope, fixed-width integer and Boolean operations, functions and calls, defaults and keyword arguments, static overload selection, conditionals and loops, compile-time-anchored range values, streamed and materialized string interpolation, grapheme operations, homogeneous arrays, structural objects, optional values and narrowing, initial dictionary literal iteration, explicit nonescaping places, source modules and imports, and the initial Time/Duration facilities.
Important partial areas include arrays whose storage requirements escape their current scope, interpolation through user-defined conversions, ranges with runtime anchors or runtime storage, general physical dimensions, function handles and closures, runtime dictionary operations, and broader host support.
Major design or implementation frontiers include floating-point and exact real arithmetic, nominal type of Parent creation and hybrid construction, structural-object intersection merging, general user-written generics and unannotated generic inference, growable dictionaries and sets, complete refinements and effects, exception-value forwarding and recovery, broadcasting and multidimensional array operations, pattern matching and general unions, generators as stored values, and general compile-time evaluation.
This summary is intentionally broad. The repository's implementation status is the authoritative detailed checklist.
Platform Notes
The quick installer and the full hosted execution path currently target x86-64 Linux. The µDewy bootstrap compiler and browser playground cover additional backend and WebAssembly scenarios, but the browser playground runs µDewy rather than the complete Dewy language.
Platform availability is an implementation property, not a restriction in the language design.
Reading Examples
Unless a chapter says that a design is provisional, its examples illustrate intended Dewy. Some examples may be ahead of the current compiler. Examples that are part of compiler tests are checked continuously; future documentation tooling will make this classification explicit in source metadata while keeping the reading experience uncluttered.
For exact present-day behavior, use the implementation status and executable tests. For exact intended language rules, use the Dewy Language Reference.