charged - pannous/warp GitHub Wiki
Code is data that is charged: it carries code that fires later. This page is the specification of when an expression is evaluated. Where another page (evaluation, assignment, literal, data, code, block, full-evaluation) disagrees, this page wins.
Goal: as general as lisp (code is data, data can be run, functions can take unevaluated arguments), and as foolproof as possible: the timing of every expression is visible where it is written, a typo never silently becomes data, and every surprising case is a loud diagnostic at the place where it was written.
Every page of this wiki uses these words in this sense:
| word | meaning |
|---|---|
| value | the result of an evaluation: x = 1+2 binds the value 3 |
| charged | carries code that fires at every use or call: f := e, def f β¦, a function |
| uncharged | inert until ! fires it: x : e
|
| block | unevaluated code meant to run (x : e, {statements}, code e, a block parameter); its type is block
|
| data | unevaluated content never meant to run: a data file, data e, plain words |
| code | what a program consists of; code e is a synonym of block e
|
| closure | a function value (lambda) together with the variables it reads |
| group |
( β¦ ): evaluated on the spot |
| symbol | an unknown word on its own: red
|
! / !! |
force: run a block (once / fully, full-evaluation), unwrap an optional, a plain value stays itself |
| literal | text taken literally: strings, templates, literal arguments (import graphics) |
- A program (a
.waspfile run or compiled, the playground,eval) is code. - A data file (
warp data <file>,parse_data,ParserOptions::data()) is data: nothing is evaluated,(cube 3)[cube 3]{cube 3}are lists,print 3is the symbolprintand the number 3. - Inside a program, data is written explicitly (section 4). A later script mode (the shell, REPL) may keep assignments symbolic as evaluation describes; it is not defined yet.
This is the main difference to lisp: lisp quotes data inside code; wasp needs to mark it only there, and a whole file of data needs no marking at all.
One rule everywhere in code, brackets change nothing:
-
A word on its own is a symbol:
red,[red green],{color: red}withredundefined are the symbolred(enum-like data). A got-it warning appears only when it is one edit away from a defined name ({age: agee}β "did you mean age?"). -
A word applied to an argument, or called with empty parentheses, is an error:
cube 3,cube(3),cube(),[cube 3]withcubeundefined are the errorundefined function: cube. Writedata [cube 3]for the data.
| form | kind | evaluated | type of the name |
|---|---|---|---|
x = expr |
value (neither charged nor uncharged) | now, once | the value's type T
|
f := expr, f x := expr, f be expr
|
charged (expression form) | on every use | a function () β T / X β T
|
def f(x): β¦, def f {β¦}, to f x: β¦, fun f(x){β¦}
|
charged definition (def = deferred) | on every call | a function |
x : expr |
uncharged |
never, until x!
|
block |
Charged means it carries code that fires later, at every use. Uncharged means an inert block that fires only when
! charges it. A value has already fired.
:= and def share one timing: := is the expression form, def the statement form. Both behave like Python's
def (section 3). A := without parameters is always charged (user decision P71, 2026-10-06): a getter that
evaluates its expression at every use and reads the variables as they are then; assigning it afterwards (z = 6,
z += 1) is an error that names the definition. Inside an object, key := value is a getter entry too (section 4).
y = 3
x = y*y // value: 9
z := y*y // charged
w : y*y // uncharged: the block y*y
x // 9
z // 9, evaluated at this use
w // y*y, a block
w! // 9
y = 4
z // 16: a getter reads the current y
Not getters: f := {β¦} (a block), f := x => β¦ and f := it*2 (functions), and a name applied to arguments later
(sum := fold +; sum [1 2 3], a function value).
-
Free variables are read at call time (Python's late binding), but a free variable that changes after the
definition must be declared:
y=3; def z(): y*y; y=4; z()is a compile error aty=4: "z reads y (line 1): declareglobal yin z to read its current value, or pass y as a parameter". Withy=3; def z(): global y; y*y; y=4; z()β 16. Details:- a change counts only where a later call can see it: a call in a later statement, or a loop that both changes
the variable and calls the function (
x=1; inc:={x+1}; x=inc(); xis 2, no error); - a free variable that is never reassigned needs no declaration (it is a constant, see below);
-
globalmay stand on either side: in the reading function (def z(): global y; β¦) or on the variable at the main level (global limit = 10; def ok(x){x < limit}), which makes it shared for every function; - in-place mutation counts as a change (
xs.add(1)for a list z reads); - a variable of an enclosing function is declared
nonlocal y(Python); -
global ymarks z as reading shared state: z is not pure, so it is neither folded nor memoized. Python needsglobalonly for writes; wasp also for reading a changing variable, so the dependency on later state is always visible in the function.
- a change counts only where a later call can see it: a call in a later statement, or a loop that both changes
the variable and calls the function (
-
Loop variables need no declaration: a function created inside a loop that reads the loop variable captures
that iteration's value.
fs = []; for i in 0..3 { fs.add(x => x + i) }; fs#1(0), fs#2(0), fs#3(0)gives 0, 1, 2, not Python's 2, 2, 2. -
Assigning an outer variable inside a body needs
global x(Python'sglobal); without it the assignment is a local and a hint namesglobal x. - Default arguments are evaluated per call, not once at definition (Python's shared mutable default is a footgun, Footguns).
-
Call arguments are evaluated before the call (strict), unless the parameter is declared
block(section 5). -
Getters are the exception: a
:=without parameters reads a changed variable withoutglobal(y=3; z:=y*y; y=4; zβ 16, P71): reading the current value is what a getter is for, and its definition says so. -
Effectful getters: a zero-parameter charged definition whose body has effects (
id := next_id(),now) is allowed but gets a got-it warning at the definition: "id runs next_id() at every read; write id = β¦ for one value". Pure bodies get none: for a pure expression the timing cannot be observed (wasp's effects). -
Needless charging: a zero-parameter charged definition whose body is pure and reads only constants
(
area := 3*4) is computed once (folding, below) and gets a got-it note at the definition: "area never changes: write area = 3*4".
"Every call" is the meaning, not the machine code. The compiler may compute anything earlier whenever no program can tell the difference, that is for every pure part whose inputs are known:
-
Constant calls are folded: a pure call with constant arguments is computed at compile time and replaced by its
value (
def fib(n): β¦; fib(10)compiles to55). -
Constant free variables are folded: a free variable that is never reassigned after the definition (no
late binding can be observed) is inlined as its value; a reassigned one must be declared
global(above) and stays a read at call time. - Invariant parts are hoisted: inside a body, a pure subexpression that depends on neither the parameters nor a reassigned variable is computed once (at compile time, or at the first call), not at every call.
-
Pure functions may be memoized: results of a pure function may be cached per argument (
fibwithout exponential blow-up); the compiler decides alone, there is no syntax for it. -
Specialized paths: a call with some constant arguments may get its own precompiled variant (partial
evaluation), e.g.
power(x, 3)βx*x*x. -
Never precomputed: anything with effects (I/O,
now,random, mutation, task operations, run-time!) and every read of a variable that is reassigned; these run at every call, in order.
So def is Python's semantics with a compiler's speed: the observable behaviour is per call, the work is done as
early as purity allows. The same applies to :=.
Two words for two intentions:
- A block is code meant to run later:
x : expr, or the prefixcode expr/block expr(synonyms). Its type isblock;x!runs it. -
data is never meant to run: the prefix
data expr(data [cube 3]), and plain words (section 1).!on data is allowed but gets a got-it warning ("running data as code").
Both are unevaluated Nodes; the difference is the intention, which decides the warnings. The word quote is not used (quote redirects here).
{ β¦ } in a program is data unless an entry says otherwise:
-
key: valueentries are uncharged (a literal or a plain word is simply that value or symbol); -
key = valueentries are values, evaluated now, once, where the object is written; -
key := valueentries are getters (P71, as in section 2):o = {a: 1, s := clock()}reads the clock at everyo.s, and the body reads the object's other fields and the variables as they are at that read; - statements make a block:
x = {1+2}is the block1+2(x!β 3), while the group(1+2)is evaluated on the spot (blocks are passed around lazily, groups get evaluated on the spot); - control flow takes blocks as
blockparameters (section 5):if c {β¦},while c {β¦},for x in xs {β¦}anddef f {β¦}run their block when they decide to, so they need no special rule.
a = 1; b = 2
o = {
s1: a+b // uncharged: the block a+b; o.s1! is 3
s2 := a+b // getter: a+b at every read of o.s2
s3 = a+b // value: 3
age: 3 // a literal is the same under every form: no warning
color: red // the symbol red
}
Footgun guards:
-
At the definition, not at the use: a computed expression after
:in code (s1: a+b, not a literal and not a plain word) gets a got-it warning where it is written: "s1 keeps the block a+b; write s1 = a+b for its value". Literals and words never warn, so JSON-shaped data stays silent. -
The type is visible: using a
blockwhere a value is needed (o.s1 + 1) is a type error with the fix (o.s1!ors1 = β¦), never an automatic evaluation. -
Not uncharged:
:keeps its other meanings, decided by position: a type after a field name declares a field (person{name:text age:int}), named call arguments are evaluated (f(x: a+b), call arguments are strict), the colon of a definition or branch (to square x: x*x,if c: x else y) is syntax, and a data file is all data.
-
x!forces x, "get the real value now": a block runs, an optional gives its value or the errorunwrapped ΓΈ, a plain value is itself. The kind decides, statically where it is known, else at run time. -
x!runs a block once: one level is removed.x!!(orinterpret x) evaluates fully, including nested blocks (full-evaluation). -
Names inside a block resolve where
!is written (lisp's eval, not a closure). Because this is not hygienic, when the block is a constant (known at compile time, the usual case) every name is resolved at compile time at the!site and an unresolved one is an error there; a name that resolves to a different binding than one visible where the block was written gets a got-it warning. -
help = {print: "there is help"}; help!runs the object as code: eachkey: valuebecomes the callkey(value)(print "there is help"). -
Lisp macros without macros: a parameter declared
block(synonymcode) receives its argument unevaluated, and the body runs it with!when it wants to:unless(c, body:block) := if !c: body!. The laziness is visible in the signature, so the call site looks normal and the IDE marks such arguments.
A block known only at run time (built by the program, read from input) is compiled when ! runs it: the program
hands it to warp's compiler through a host import and runs the result in the same store, sharing its values:
-
Rights: a block that arrives at run time is pure by default: no I/O, files or FFI unless the program grants
them explicitly (
x! with io); data read at run time is tainted (likeeval_untrusted). -
Outside warp's runners:
warp compileof a program with run-time!gives a warning, and the module loads a shared compiler lazily the first time!runs. - Run-time
!is an effect: its function and every caller are not pure (no folding, no memoizing), and everything visible at the!site stays in the module. - A run-time block reads the
!site's local variables as they are when!runs and assigns only declared globals.
| data file | program | |
|---|---|---|
( β¦ ) |
list | group or call, evaluated on the spot |
[ β¦ ] |
list | list of evaluated elements; plain words are symbols (section 1) |
{ β¦ } |
list / map | data by default: an object (section 4) or a block |