A block is a few lines of logic that belong together. It holds interim calculations under local names, and its last line, written without a semicolon, is its value. The names live only inside it. A block can go wherever an expression can: as the definition in a derive, as an argument, as a default. This is Cone's block (refblock.html), unchanged except that nothing in it is about memory.

A world adds one rule Cone does not have. A block is either a value block, which defines a value and may only calculate, or an acting block, which answers an event and may calculate and then act. The reason is the paradigm: the engine recalculates values when and as often as it chooses, so an act inside a value would happen an unknown number of times in unknown places.

How you write a block

As in Cone, a block is a sequence of statements in braces, each ended by a semicolon; the braces alone decide where a block begins and ends, and indentation is for the reader (refblock.html, "Statements", "Blocks"). Its value is its last line, written without a semicolon (a semicolon throws a value away, so a block whose last line ends in one gives no value):

{
  let sum = a + b;
  let mult = a * b;
  sum / mult           // the block's value
}

Wherever an expression may stand, a block may stand:

derive(flame.firing, {
  let t = now(sim);
  ...
  pulse(from: launched, every: sim(5 s), for: sim(3.2 s), at: t).contains(t)
})

A block is written in Cone's form, in braces with semicolons (none after a last line that gives the block's value): let is Cone's let, a function is written fn, and the block's value is its last line, written without a semicolon. World declarations (part, field, derive, on) keep their indented notation; a block inside them is in braces.

Parts inside each and if

In a part's body, the braces of an each or an if may hold part declarations beside Cone statements. Each turn of the each declares its parts again, named from the loop's values; an if declares its parts only when its condition holds. The condition and the loop's list are fixed when the part is built: they read fields that do not change (a design's panel, a count), so the parts are made at build like any other (Loading). A statement ends with a semicolon; a part line is a declaration and takes none, so the two kinds are told apart by their first word.

part pathLanterns
  each p in 0 ..< 6 {
    let way  = land.walkway;                                         // a statement
    let spot = way.spotAt(way.fromEnd(1.8 m + 2.8 m * p), across: 1.5 m);
    part lantern[p]: pathLantern, at: spot                             // a declaration, once a turn
  }

part lanternHead
  field panel: ?shape = None
  if let Some(p) = panel {
    part solar: scale(p, by: size), material: panelIn                  // only for a head given a panel
  }

The parts an each declares are a numbered family, lantern[0] to lantern[5], named with an index as a list's element is (Expressions: paths).

A shape is a function, fn name(...) shape { ... }, and its body is an ordinary block: its value is its last line, the shape it makes, and it holds no part lines (Construction).

Joining shapes: join([...])

join(shapes: List[shape]) is an ordinary function that takes a list of shapes and gives one shape of them all. Nothing is welded: the pieces are one shape of several separate surfaces (Shape tools: joining). A shape made of several pieces always says join, since a shape made by a statement and ended by a semicolon is thrown away like any value.

fn wallBracket() shape {
  join([
    box(x: -0.04 ..= 0.04,   y: -0.07 ..= 0.07,   z: 0 ..= 0.015),      // a plate on the wall
    box(x: -0.015 ..= 0.015, y: -0.015 ..= 0.015, z: 0.015 ..= 0.17)    // an arm 17 cm out
  ])
}

Where the pieces come from a loop, build the list with Cone's own collection form, List[shape]() <- ..., each ... yield ..., then join it. An if choosing between pieces is an ordinary if whose branches are shapes:

fn bracketRing(spokes: count, plated: bool) shape {
  let pieces = List[shape]() <- box(x: -0.02 ..= 0.02, y: 0 ..= 0.3, z: -0.02 ..= 0.02),
                               each f in 0 ..< spokes yield turn(arm, by: f * 90 deg, about: up);
  if plated { join(pieces) } else { box(x: -0.04 ..= 0.04, y: 0 ..= 0.02, z: -0.04 ..= 0.04) }
}

Nested loops build the list in a var pieces = List[shape](); with pieces <- ...; inside the loops, or run one loop over 0 ..< 16 and read the row and column as k % 4 and floor(k / 4). Joining may take a second, named option for how the pieces meet, join(pieces, how: blend(1.5 cm)).

Local names

A local name is declared with let (its value never changes) or var (it may be given a new value), as in Cone (refterm.html, "Variable declarations"):

let height = 1.86;          // fixed
var total = 0;              // changeable
total += 4;

Its type is usually inferred from its value, and may be written: let height meters = 1.86 m;. A local lives from its declaration to the end of its block; an inner block sees the outer block's names, and the outer does not see the inner's (refblock.html, "Local Variables").

A local name may not hide a name its definition reads. Cone allows an inner name to hide an outer one and advises against it (refblock.html, "Local Variables"). ConeScript refuses it when the hidden name is read by the block's own definitions, because that is where it caused a silent mistake: the balloon's course has a point called the summit, and the moment the ride reaches it was first written with the same name, so let top = launched + ride.timeAt(top) read as if the moment were defined from itself. The point is called summit and the moment reachedSummit.

Value blocks

What it is. A block that defines a value: the definition in a derive, a default, an argument, the body of a function called from any of these. It is a pure calculation.

What it may not do. It may not set, start, spawn, send, say or play, or perform any other act that changes the world (stop, remove). It may change its own var locals: those are not the world.

What it may do. Calculate, name interim values, loop, branch, call functions that do not act, and make observations that change nothing in the world, such as writing a line to a debug log.

Why. The engine recalculates a value when and as often as it chooses: every frame, only when an input changes, on the server, on each visitor's machine. An act inside a value block would happen an unknown number of times in unknown places. Keeping value blocks pure is what makes recalculation safe, and so makes the spreadsheet model possible at all. Cone has the same idea as an option, the pure function, which may not change state or do input and output (reffunc.html, "Pure functions"); in ConeScript every value block is pure by rule.

Example: the balloon's burner. The burner fires in bursts, and which burst is in force is worked out in a few named steps. The rhythms: on the ground 3 s on in every 4; climbing to the summit 3.2 s in every 5; after it 1.5 s in every 20; in the last 45 s before landing nothing, a burst in progress cut short where the quiet begins; after landing the ground's rhythm, counted from the landing. The ride is 170 s.

part balloon
  field launched: moment = never, mutable: true, lives: server
  derive(flame.firing, {
    let t             = now(sim);
    let landed        = launched + ride.length();              // 170 s after launch
    let reachedSummit = launched + ride.timeAt(along: course.pointNamed(summit))   // about 70 s in
    let quiet         = landed - sim(45 s);
    let burst = if launched == never {
      pulse(from: start(sim), every: sim(4 s), for: sim(3 s), at: t)     // the clock's zero: the moment the world began
    } else if t >= landed {
      pulse(from: landed, every: sim(4 s), for: sim(3 s), at: t)
    } else if t < reachedSummit {
      pulse(from: launched, every: sim(5 s), for: sim(3.2 s), at: t)
    } else if t < quiet {
      pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: t)
    } else {
      until(pulse(from: reachedSummit, every: sim(20 s), for: sim(1.5 s), at: quiet), quiet)
    };
    burst.contains(t)
  })
  derive(roar.gain, if flame.firing { 1.5 } else { 0 })

A value that takes several steps to say is still a value: written as a block it stays a pure formula of the clock and one stored moment, so a visitor who arrives mid-ride sees the right burst.

The calls it uses. They are plain functions of moments:

CallTypeWhat it is
pulse(from: moment, every: duration, for: duration, at: moment)spanthe burst of the period that holds at, the periods counted from from: it begins at from plus a whole number of periods and lasts for. Before from it is the first period's. It is returned even when at is past its end, so contains tells whether it is on
until(s: span, m: moment)spanthe same span, cut short where m falls inside it; unchanged when it ends sooner
spantypea stretch of time with start and stop (moments), and s.contains(t), true from start up to but not including stop
start(sim)momentthe clock's zero: the moment the world began. A ground rhythm counted from it is on from the first moment, before any ride

The summit is a named point of the ride's course: course.pointNamed(summit) is its distance along, and ride.timeAt(along:) is the time the ride reaches that distance (Animators: asking an animator).

Acting blocks

What it is. A block that answers an event: the block given to on(trigger, ...). It may calculate first, under local names, and then act. Its statements are done in order.

on(action(ask, q, to: g, by: v), {
  let inReach = within(v, g, 4 m);
  if inReach && asked[v] {
    say(g, "You have had your question.", to: v);
  } else if inReach {
    set(asked[v], true);
    say(g, answer(g, q), to: v);
  }
})

When a being asks a guard a question, first work out whether it is within 4 m; if it has asked before, the guard says so; if not, mark it as having asked and say the answer.

Calculate, then kick something off

"Calculate something and, when it becomes true, act" needs no third kind of block. It is two lines: a value, and a rule that watches it.

derive(home, launched != never && !riding)
on(became(home), play(sound.chime, at: basket))      // illustration

The rule fires once, at the moment home becomes true, however often home is recalculated, and the purity of the first line is kept.

Where blocks run, and for how long

How it connects

Open questions

← Expressions and operators | Contents | Functions →