An expression computes a value from other values: glow * 0.09, now(sim) - launched < sim(170 s), honest(g). Expressions are the bottom layer of a world's logic: the lantern's height from the ground, a glow's share for the glass. In ConeScript an expression may name the world's own values (a field of this part, a field of another part by its path, the clock), so an expression is also a statement of what depends on what. When an expression defines a field through derive, the engine recomputes it whenever anything it names changes, as a spreadsheet does (Subscriptions).
Operators, calls and precedence are Cone's (refexpr.html, refif.html). This page lists them, then adds what a world needs: units in arithmetic, field access on parts, paths, indexing by being, and named arguments.
Arithmetic
| Operator | Meaning |
|---|---|
+ | add |
- | subtract, or negate as a prefix |
* | multiply |
/ | divide |
% | remainder |
Precedence is algebra's, and parentheses override it (refexpr.html, "Arithmetic Operators"):
2*2 + 2*3 // 10 2 * (4 + 1) // 10
Units in arithmetic. The units of the operands decide the unit of the result, and a mismatch is refused (Values: units):
launched + ride.length() // a moment plus a duration: a moment now(sim) - launched // a moment minus a moment: a duration now(sim) - launched < sim(170 s) // durations compared: a bool materials.lanternBulb.glow * 0.45 candelaPerNit * gain // nits times candela per nit: candela
The last line is a lantern's light. Places, offsets and directions are added, subtracted and scaled as wholes, and a rotation turns a direction: rack + lane * (row * 0.72 m), rotation(-90 deg, about: up) * lane (Values: arithmetic on places and directions).
Maths functions
These are every maths function a world may call, in shapes, rules and formulas alike. Each is called by name with its arguments (the argument rule), and each says what it does with units. Units follow algebra (Values: units multiply and divide as in algebra): most functions keep the unit of what they are given, sqrt halves it (square meters to meters), and only a few need a plain number, because their answer would mean nothing in a unit: exp, ln, log2, pow with an exponent that is not a whole number, and the trigonometric functions, whose argument is an angle or a plain number. A unit one of those cannot take is refused when the world is checked; divide it out first (ln(b / 1 nits)).
| Call | Result | What it gives | Units |
|---|---|---|---|
min(a, b), max(a, b)min(xs), max(xs) | as a, or as the elements of xs | the smaller, the larger. Given one list, the smallest or the largest element of it: max([a, b, c]). An empty list is refused | both in one kind of unit, or both plain; the elements of a list likewise; the result in it |
clamp(x, within: range) | as x | x held within the range: its low end if below, its high end if above | x and the range in one kind of unit |
abs(x) | as x | x without its sign | keeps the unit |
sign(x) | number | -1 below 0, 0 at 0, 1 above | takes any unit; gives a plain number |
floor(x), ceil(x), round(x) | as x | the whole number of x's unit at or below x; at or above it; nearest it (a half rounds away from 0). A plain result is a whole number, and may index a list | keeps the unit: floor(2.7 m) is 2 m. The answer depends on the unit written (floor(250 cm) is 250 cm, 2.5 m), so divide by the step you mean: floor(d / 2.5 cm) |
fract(x) | as x | x minus floor(x): the part after the point, 0 up to 1 of the unit | keeps the unit, as floor |
sqrt(x) | the root of x's unit | the square root; refused below 0 | halves the unit: square meters give meters, a plain number a plain number |
pow(x, p) | number, or x's unit to the p | x to the power p | p plain; x may carry a unit only when p is a whole number (pow(r, 2) is square meters) |
length(v) | as v's numbers | the length of an offset or a direction (x, y, z) | keeps the unit: an offset in meters has a length in meters |
distance(a, b) | as the points' numbers | the straight-line distance between two places or points | both in one kind of unit; the result in it |
planDistance(a, b) | as the places' numbers | the distance between two places measured on x and z only, as on a map: their heights are ignored. The land's rules use it, since they read only x and z (Terrain) | both in one kind of unit; the result in it |
gradient(f, at: p, step: meters)highest(f, every: meters) | a direction; as f | two questions about a function of position f (an open function) that a world asks of its own fields. gradient is the slope of f at the place p as a vector on the plan (x, 0, z), rise over run: the difference of f a step either side of p on x, and on z, each divided by twice the step. highest is the largest value of f over a lattice of places every apart, over the whole of the land's grid from its corner (-1024 m, -1024 m): a whole-field question, asked before any single point, whose lattice is part of the question | step and every lengths; gradient plain; highest in the unit of f |
circle(center: point, radius: meters)capsule(from: point, to: point, radius: meters)distance(p, region) | a region; meters | a region is a flat area on the plan, read on x and z only. A circle is the area within radius of center; a capsule is the area within radius of the straight line from from to to. distance(p, region) is the signed distance from the place p to the region's edge: negative inside the region, 0 on the edge, positive outside. A region answers only distance: it is not a shape and draws nothing, and it is distinct from the shape tools (Appendix: shape tools), whose outline.circle is a flat outline to sweep along a spine | the points and radius in meters; the result in meters |
smoothMin(a, b, width: meters), smoothMax(a, b, width: meters) | as a | the smaller, the larger, rounded where the two meet. With h = clamp(0.5 + 0.5 * (b - a) / width, within: 0 ..= 1), smoothMin(a, b, width: w) is lerp(b, a, t: h) - w * h * (1 - h), and smoothMax(a, b, width: w) is -smoothMin(-a, -b, width: w). Where a and b differ by width or more the answer is exactly min or max. The formula is written out because the land must come out the same on every machine. Of two signed distances, smoothMin is the two areas joined smoothly; smoothMax of one distance and the negative of another cuts the second area out of the first | a, b and width in one kind of unit; the result in it |
within(a, b, d) | bool | true when the 3-D distance between the places a and b is under d. A part or a being stands for its place, and a being's place is its feet: within(v, g, 4 m) asks whether two beings' feet are less than 4 m apart | a and b places, d a length |
nearest(xs, to: p, within: d) | an element of xs, or None | the element of a list of parts or beings (copiesOf(guard), visitors) whose place is nearest the place p; None if the list is empty or no element is within d. to: and within: may be left out where the first has a natural center (nearest(visitors) is the visitor-driven being nearest at this step) | p a place, d a length |
filter(xs, f) | List of the elements of xs | the elements for which f is true, in their order, beside map(xs, f), which gives a list of what f returns for each: filter(rocks, r => brook.nearest(r.place, within: 1.4 m) != None) is the rocks within 1.4 m of the brook | f a function of one element giving a bool |
nearestFew(xs, to: p, within: d, count: n) | List of the elements of xs | the elements of a list whose place is within d of the place p, the n nearest at most, as a list, nearest first; an empty list if none is within d. nearestFew(copiesOf(guard), to: v.feet, within: 20 m, count: 3) is the three guards nearest a visitor (an illustration) | p a place, d a length, n a count |
dot(a, b), cross(a, b) | number; three numbers | the sum of the coordinates' products (0 at right angles); the three numbers at right angles to both, by the right-hand rule | units multiply: two directions give a plain number, two offsets in meters square meters |
normalize(v) | direction | the same way as v, of length 1; refused for (0, 0, 0) | takes any unit; the result plain |
mix(a, b, t, in: linear | srgb) | as a | for two colors, the color the fraction t of the way from a to b, on the linear numbers (the default) or the sRGB ones (Values: colors); for two rotations, the rotation t of the way, the shortest way round (in not given) (Values: rotations) | t plain |
exp(x), ln(x), log2(x) | number | e to the power x; the natural logarithm; the logarithm to base 2 (both refused at or below 0) | plain numbers only: ln takes a plain number, never a quantity, since the log of 800 nits means nothing (a logarithmic curve blends quantities without one) |
sin(a), cos(a), tan(a) | number | the sine, cosine and tangent | a an angle, with its unit (22.5 deg), or a plain number of radians |
pi | number | 3.14159...; half a turn is also 180 deg, which usually reads better | plain |
asin(x), acos(x) | angle | the angle whose sine, or cosine, is x (from -1 to 1) | x plain; the result an angle |
atan2(y, x) | angle | the angle of the direction (x, y) from +x, counterclockwise | y and x in one kind of unit, or both plain; the result an angle |
lerp(from, to, t: fraction) | as from | the value the fraction t of the way from from to to | from and to in one kind of unit; t plain |
smoothstep(x, from, to) | fraction | 0 at or below from, 1 at or above to, an S-shaped rise between, flat at both ends. The edges may come either way round: with from above to the answer is 0 at or above from, 1 at or below to, and runs from 1 down to 0 as x rises between them (smoothstep(h, from: 2.7 m, to: 1.3 m) is 1 below 1.3 m and 0 above 2.7 m) | x, from and to in one kind of unit; the result plain |
remap(x, from: range, to: range) | as to | x's place in the one range carried to the same place in the other, in a straight line, not held at the ends (clamp it to hold it) | x in the unit of from; the result in the unit of to |
noise(p, size: meters, seed: count) | number | one octave of smooth noise at the point (or flat point) p, read on its x and z (a flat point on its two numbers): a value from -1 to 1 that changes gently, with bumps about size apart; 0 at every lattice point. Deterministic: the same point and seed give the same value on every machine. The noise functions below, and warped, take p, size and seed alike | p and size in one kind of unit; the result plain |
noise.fbm(p, size: meters, octaves: count, seed: count, lacunarity: number = 2, gain: number = 0.5) | number | fractal noise: octaves layers of noise added up, the first at size with weight 1, each next at lacunarity times the frequency (so size divided by it) and gain times the weight, each layer with a seed of its own made from seed and its number. The sum is not divided by the total weight, so it ranges within ±(1 + gain + gain² + ...) up to the octaves: within ±1.97 for six octaves at the default gain, and never beyond ±1 / (1 - gain) | as noise |
noise.ridged(p, size: meters, octaves: count, seed: count, lacunarity: number = 2, gain: number = 0.5) | number | ridged noise, added up the same way, but each layer is (1 - abs(n)) squared for the layer's noise n: from 0 to 1, with sharp ridges where n crosses 0. The sum ranges from 0 to (1 + gain + gain² + ...) up to the octaves: 0 to 1.97 for six octaves at the default gain | as noise |
warped(p, by: meters, size: meters, octaves: count, seed: count) (to be built) | point | the point p moved sideways by noise, for roughening a coast: x is moved by by times noise.fbm of p at size and octaves with seed, and z by by times noise.fbm of the same with the lattice shifted by (17, -9) cells and the seed two higher. The island's coast is warped by 40 m over 300 m, three octaves, seed 11. The noise package's own warp (warp2) is Quilez's two-level warp of a value, a different function | as noise; the result a point, moved by up to by times the noise.fbm range |
noise.phacelle(p, dir: direction, freq: number = 1, offset: number = 0, normalization: fraction = 0.4, seed: count) | a stripe record: .depth, .slope | oriented stripes with a continuous phase, for gullies. p is in cells, a point divided by the spacing of the stripes (here / size); dir is the direction the stripes run along at p, of length 1, and may change from point to point; freq, best kept near 1, is the stripes' frequency within a cell; offset shifts the phase, 1 being a whole cycle; normalization, 0 to 1, is how far a blended wave weaker than full strength is lengthened to full (0.4: one below 0.6 is not fully lengthened). .depth is a fraction, 0 on a crest and 1 on a gully's floor; .slope is how .depth changes across the stripes, a flat vector on the plan, a plain number for each cell of p: divide it by the spacing to have it for each meter. The package's own result is the wave value (its cosine and sine) and the side direction across the stripes, times 2 pi times freq: .depth is (1 - cosine) / 2 and .slope is sine times side / 2. Not bit for bit the same on a graphics card as on the main processor | p plain; dir plain; both results plain |
hash(seed, k) | fraction | a number from 0 up to 1 that looks random but is fixed for the seed and k, a whole number or two or three in a tuple (hash(seed, (i, j)) for a grid cell). Deterministic: no stream, no order. Defined exactly below | plain whole numbers, of either sign; the seed a count |
hashWord(seed, n) | fraction | the one-word hash: a number from 0 up to 1 fixed by the single whole number seed + n. It is a different function from hash, and defined below; the island draws the hosta leaves' vein phase with it, 6 * hashWord(seed, 3) | plain whole numbers; the sum wraps round as a 32-bit word |
What hash is, exactly. So that a hash gives the same number on every machine, and the same number the noise package and the pattern recipes draw, it is defined to the bit. Its arithmetic is on 32-bit words (Cone's u32): multiplying and adding wrap round past 232, ^ is exclusive or and >> shifts right. A whole number k becomes the word of its two's complement (-1 is 4294967295), and so does the seed. Then, with m = 1664525 and c = 1013904223:
hash(seed, k) = unit(pcg2d(k, seed).0)
hash(seed, (i, j)) = unit(pcg3d(i, j, seed).0)
hash(seed, (i, j, l)) = unit(pcg4d(i, j, l, seed).0)
unit(h) = (h >> 8) * 2^-24 // the top 24 bits, exactly: 0 up to 1
pcg2d(x, y): x = x*m + c; y = y*m + c;
x += y*m; y += x*m; x ^= x >> 16; y ^= y >> 16;
x += y*m; y += x*m; x ^= x >> 16; y ^= y >> 16; gives (x, y)
pcg3d(x, y, z): each = each*m + c;
x += y*z; y += z*x; z += x*y;
x ^= x >> 16; y ^= y >> 16; z ^= z >> 16;
x += y*z; y += z*x; z += x*y; gives (x, y, z)
pcg4d(x, y, z, w): each = each*m + c;
x += y*w; y += z*x; z += x*y; w += y*z;
x ^= x >> 16; y ^= y >> 16; z ^= z >> 16; w ^= w >> 16;
x += y*w; y += z*x; z += x*y; w += y*z; gives (x, y, z, w)
hashWord(seed, n) = unit(pcg(seed + n)) // the 32-bit words added, wrapping round
pcg(v): s = v*747796405 + 2891336453;
w = ((s >> ((s >> 28) + 4)) ^ s) * 277803737; gives (w >> 22) ^ w
hashWord is the one-word PCG (O'Neill's RXS-M-XS), the word that also steps a scatter's random stream (Generators: the stream, exactly). The three hashes above are Jarzynski and Olano's PCG hashes. 2 * hash(seed, k) - 1 is a number from -1 up to 1, exactly the noise package's signed form of the same word. A recipe or a spell that must draw exactly the numbers another package draws (the island's zap shakes, the audio mixer's footsteps) uses that package's own hash; hash stays as defined here.
let t = smoothstep(hour(now(world)), from: 17, to: 18); // 0 before 17:00, 1 after 18:00 let r = sqrt(p.x * p.x + p.z * p.z); // the distance from the y axis: m times m is m², its root m let r = length((p.x, 0 m, p.z)); // the same, as an offset's length let pitch = remap(speed, from: 0 m/s ..= 10 m/s, to: 0.8 ..= 1.4); let cell = (floor(u / 2.5 cm), floor(v / 2.5 cm)); // plain whole numbers: a grid cell let jitter = 2 * hash(seed, cell) - 1; // -1 to 1, the same for the cell everywhere let n = ln(b / 1 nits); // a log needs a plain number: divide the unit out
/ always divides exactly: gores / 8 is 3 for 24 gores and 3.125 for 25. A whole-number division is floor(a / b), and % gives the remainder. A list is indexed by any number whose value is whole, so panel[floor(u * gores) % 3] needs no conversion; an index with a fraction is an error (Indexing).
Names follow Cone's rules. A name is written in camelCase, as the packages write theirs (lampLevel, reachedSummit, lanternHead), and a hyphen is always a minus: landed-sim(45 s) is a subtraction, the same as landed - sim(45 s). Whether names should allow hyphens instead is open.
Comparison
x == 3.0 // equal: same value and type x != y // not equal x >= 2 // ordered comparisons: <, <=, >, >= g != liar // part references compare by which part they name
Equality is ==, as in Cone (refif.html, "Equivalence"); a single = only gives a value (a default, a local name's value, a named argument's value is :). Each type decides what equality means for it; two lists are equal when they hold the same values.
Boolean operators
near && lampLevel > 0 !riding a < 3 || !done && b == 0 // '!' binds tightest, then '&&', then '||'; no parentheses needed
&& and || stop as soon as the answer is known, so the right side is not evaluated when the left decides (guide-conditionals.html). All three come after the comparisons.
Calls and named arguments
A function is called by its name and its arguments in parentheses, evaluated left to right (refexpr.html, "Function calls"):
honest(g) answer(g, q)
Most of the world's vocabulary is called with named arguments, name: value, because a call such as an animator's has many settings and most have defaults:
transition(lampLevel, to: 1, over: world(15 min), begin: dusk.due) sphere(radius: 4 m, at: (0, 0.2, 0)) rocks(seed: 12, width: 1.4)
The argument rule, one rule for every call and every copy:
- A parameter without a default is required: it may be given by position, in declared order, or by name.
- A parameter with a default must be named. Giving one by position is an error.
- In a call, positional arguments come first, in declared order; after the first named argument every argument is named.
- In a signature, required parameters are declared before those with defaults (Cone's own rule: once a parameter has a default, every parameter after it has one).
- A function that takes any number of like values takes them as one list:
together([a, b]),sequence([a, b, c]),bones([rightUpperArm, neck]),path([move(0, 0), line(40, 0), close]), a layout container's children first and then its options,column([child, child], gap: 3 px).
So subdivide(cage, levels: 2), tube(from: a, to: b, radius: 3 cm) and transition(lampLevel, to: 1, ...) read as before, and min(a, b), atan2(y, x), mix(a, b, t), srgb(r, g, b), set(path, value), say(speaker, words, to: v) and keys(target, [...]) all fit the rule: their parameters are required, so they may be given by position. The rule is the same for the shape tools (Appendix: shape tools), a world's own functions, animators, actions and the copies of a design, whose fields follow it (Designs). Cone already names values this way when it builds a struct (Point(x: 3., y: 4.), refstruct.html, "Initialization").
Methods and field access
The dot reaches a value's method or field, as in Cone (refexpr.html, "Method Calls"):
(-1).abs // 1 p.x // a position's x ride.length() // an animator's answer: the ride's length dusk.due // an event's field: the moment it was due t.0 // a tuple's first element presets.count // a list's length: the number of its elements
On a part, the same dot reaches a field of that part. It is Cone's dot, written without spaces; a part's children are reached with the same dot, so a path and a field read as one chain: the last name is a field of the part the names before it reach:
cap.offset weather.rain materials.lanternBulb.glow
The meaning is the field named on the right, of the part or value on the left.
Paths
A path names a part by its place in the world's tree: its ancestors' names from the outside in, joined by dots, the same dot that reaches a field:
glass // a bare name: found in this part, or outward from it materials.lanternBulb // found as 'materials', then its child 'lanternBulb' island.materials.lanternBulb // absolute: from the world's own name, whatever is nearer island.labyrinth.gate self.parent.lantern[13] // a neighbor: a sibling of this part, one of a numbered family self.parent // this part's parent self // this part lantern[12].glass // a numbered part: the family 'lantern', its element 12
How a name is found. A bare name, or the first name of a path, is looked up as a name is in nested blocks: first among what the current part holds (its children, fields and functions), then in the part that encloses it, and so on outward to the world's root. The first match wins. A path that starts with the world's own name (the top part's name, island here) is absolute: it starts at the world's root, whatever nearer part shares a name. In a design, the current part is the copy, so each copy's glass means its own glass, and the search then goes outward through the parts that enclose the design's text: inside lanternHead, lampLevel and materials are found on the world's root, because the design is written there. A nearer part of the same name hides a farther one, as a local hides an outer name; the world's name reaches past it. The same holds for the browser's tools: a local name hides a tool of the same name, and the tool is still reached through its package, sculpt.face (Parts: two namespaces).
A part declared in a loop is one of a numbered family, named with an index as a list's element is: the family is read as lantern, and one of them as lantern[12]. The index is any whole number, lantern[2 * p + k].
Relative names keep a design self-contained, so one lantern design can be copied forty times and each copy's glass means its own glass. A path's value is a part reference (Values: parts as values). Paths, folders and names are covered on Parts.
The path separator and division are both /. The two are told apart by what stands either side (part names, or numbers); whether that is enough is open.
Indexing
presets[2] // a list's element, counted from 0 asked[v] // a table keyed by being: being v's row thread[v]
An index is a count (Values: counts), or any plain number whose value is whole (floor, round and % of whole numbers give one); an index with a fraction, or out of range, is an error at the moment it happens. A table keyed by being always has a row for every being (its default) (Values: tables keyed by being).
Conditional expressions
if and match are expressions as well as statements: the value of the branch taken, its last line written without a semicolon, is the value of the whole (refif.html, "if as expression"; refmatch.html). An if used as a value needs its else, since every path must give a value:
derive(gainTarget, if near && lampLevel > 0 { 1.45 } else { 1 })
fn other(g: guard) guard { if g == westGuard { eastGuard } else { westGuard } }
chase(gain, toward: if near && lampLevel > 0 { nearGain } else { 1 }, speed: rate(nearGain - 1, per: sim(0.4 s)))
An option that takes a value takes an if expression as it stands, with no braces round it: toward: of a chase is if near && lampLevel > 0 { nearGain } else { 1 }, as above. The same holds for every option and argument (Animators: two kinds of chase).
A guard on actions is an ordinary if statement inside an acting block (Blocks). Both are on Control flow.
Conversions
A value is converted to another type by that type's from, as in Cone: T.from(x) (refexpr.html, "Numeric conversions"). Units of one kind convert by themselves and need no call: 1 m + 2 cm.
Assignment
Cone's assignment, =, and its shorthands (+=, -=, ++, //, parallel assignment a, b = b, a) change a local name declared var (refexpr.html, "Lval-based Expressions"). They never change a field of the world. A field is changed only by the action set(path, value) in an acting block, by an animator, or by a derive (Blocks; Actions). This keeps every change to the world visible as an act, which is what lets the engine know who writes what.
var total = 0;
each n in counts { total += n; } // fine in any block: a local changes
set(lampLevel, 1) // an act: only in a block that answers an event
What an expression means in a world
- Live. An expression that names a field or the clock is read again when they change, if it defines a value through
derive. How often, and whether a small change is worth sending, is the engine's business, never the content's. - Pure. Evaluating an expression never changes the world. An expression may call a function, but only a function that does not act (Blocks: value blocks).
- Placed by what it reads. An expression that reads a field kept only on the server is evaluated on the server (Distribution).
Open questions
- Hyphens in names. Should names allow hyphens (as CSS and Lisp do, with spaces round a minus)?
lantern-headandreached-summitread more easily thanlanternHeadandreachedSummit, but Cone has no hyphens in identifiers and readsa-bas a subtraction, so hyphenated names would cost Cone compatibility: the trade-off is readability versus Cone compatibility. This manual follows Cone's rules for now. %as remainder and as share.rate(99%, per: sim(0.35 s))uses%as a unit; Cone uses it for the remainder. Context separates them (a number directly before it, no operand after), but the double use is untested.- Derivation. Whether derivation is a relation of its own, or simply follows from expressions that mention other parts.