start
[
]
The first event triggered. This is where you should set up your trip details.
This is an overview of Timeline's documentation. If this is your first time here, it might be helpful to understand how time travel works.
❧
Timeline is a relatively simple language, syntactically. All code is executed in response to events, and all such code contains only logic or the triggering of other events. There is no I/O, logging, functions, subroutines, promises, callbacks, or anything like that.
This will build up a sophisticated example showing language features in context.
All code starts with registering an event handler via on
, and it's customary to have the first bit of code inside the start
event, which is the first even fired:
on start [] {
«code here»
}
Inside start
, you would trigger the trip details
event, which tells the Time Space Manifold where you are going, when, and what's going through the vortex. Because there is no I/O, there can be no interactive input, configuration files or the like, so you trigger everything in an event. Since our flight plan is for a single trip only, hard-coding values is not only OK, it is expected.
In this code, we include a destination using the International Celestial Reference Frame coordinate system, or ICRF. We also describe the travelers weight and a cube that completely encloses the traveler, to give a sense of size.
on start [] {
send trip details [
destination:[
ascension:60°23'42"
declination:75°02'12"
distance:384,402km
duration:1,296,000s
]
traveler:[
weight:90kg
dimensions: [
width:1m
height:2m
depth:0.3m
]
]
] now
send ping [] now
}
All event triggers match this pattern: send «event name» [ «params» ] «when»
. In this case, we send it immediately, using the keyword now
. Note that the values we are given are not naked numbers. Timeline prefers the use of well-defined data types, so instead of using 34.21222
to represent an angle, we can use the canonical encoding of an angle: 34°12'44"
. This value is unambiguously an Angle
, which makes the code much more readable than using a floating point value that you have to guess at its meaning.
You can further note that large numeric values like 1,296,000km
are typed (the km
suffix tells us it's a Distance
), but we also use commas to make the number more readable. These are required to ensure the correct transcoding of large values into the program.
You may have noticed we are also sending a ping
, which, if there are objects ahead of us in the vortex, will trigger the collision imminent
event. This event receives parameters:
on collision imminent [
distance:
position:
dimensions:
] {
buffer is 2ms
adjustment is distance - buffer
adjustment-back is adjustment + 4ms
direction is 360° - position
send adjust vortex direction [
direction:direction
] in adjustment
send adjust vortex direction [
direction:direction
] in adjustment-back
send ping [] in adjustment-back
}
You can consult the documentation for the types of the parameters (there's no need for your code to re-state what they are), but to save you time, distance
is a Time
, position
is an Angle
, and dimensions
is a Dimension
.
As we look more into this code, we can see a variable assignment. The variable buffer
is given the value 2ms
, which is a Time
. The assignment happens via the is
keyword. The equals sign (=
) used in many languages is somewhat confusing, especially when dealing with math. In math, “=
” represents equality, not assignment. Some languages create operators like :=
to allay this confusion, or even use arrows, e.g. direction ← 45°
, but we prefer the succinct and unambiguous is
. There can be no clearer way to state that the value of buffer
is 2ms than how we have designed it.
Lastly, you'll notice the expressions in use. We assign adjustment
to the result of subtracting buffer
from distance
. Since both buffer
and distance
are Time
s, and subtraction is defined on Time
to produce a Time
, the compiler (and us!) knows that adjustment
is of type Time
. There's no need to restate this information in the code.
Suppose we wish to slow down before making our turn. We can do this by checking the value of distance
. If it's less than 10ms, we trigger a custom event to slow down before the turn and speed up after.
on collision imminent [
distance:
position:
dimensions:
] {
buffer is 2ms
adjustment is distance - buffer
adjustment-back is adjustment + 4ms
direction is 360° - position
too-close? is distance < 10ms
if too-close? {
send slow down then speed up [
slow-in:1ms
speed-up-in:adjustment-back
] now
}
else {
do nothing
}
send adjust vortex direction [
direction:direction
] in adjustment
send adjust vortex direction [
direction:direction
] in adjustment-back
send ping [] in adjustment-back
}
Here we see our control structure via an if
and else
. All if
statements must have a corresponding else
, even if there is no logic needed. This creates general consistency in all code (you never have to wonder if the omitted else
is intentional), and also ensures the programmer has thought through both cases, using the do nothing
construct to explicitly indicate there is no code for the alternate case.
You'll also note that the variable too-close?
has a question mark in it. Booleans are special, since they control the flow of code. Thus, they must always end in a question mark which immediately calls out their specialness. You might think we could omit the use of this boolean in our if
statement, but if
statements may only be based upon boolean literals or variables of type Boolean
. Expressions are not allowed. This forces us to name all expressions.
Lastly, we're triggering a custom event called slow down then speed up
. How does that work? We can define it the same way as we handle built in events. The types are derived from the callsite and assumed in the code.
on slow down then speed up [
slow-in:
speed-up-in:
] {
send adjust vortex speed [
direction:90%
] in slow-in
send adjust vortex direction [
direction:110%
] in speed-up-in
}
You may be wondering why variable and parameter names are using dashes instead of underscores. We can all agree that delimiting words in names using camel-case results in a mess of unreadability, but underscores solve this nicely. Underscores are somewhat difficult to type in a QWERTY layout, requiring both pinky fingers (or a pinky and a ring finger). This results in numerous typos. Dashes are easier to execute, so we use that. When subtracting values, you must surround the hyphen with spaces to disambiguate (this also makes the code more readable).
Next, let's see the definition of the language's syntax. Note that there is significant semantic relevance to the language's constructs, for example, the syntax allows any variable to have a question mark at the end of its name, but this is only semantically allowed for booleans.
<flight plan> ::= <event handler> | <event handler> <flight plan>
<event handler> ::= on <inline-space> <event name> <inline-space> [ <whitespace> <parameter list> <whitespace> ] <whitespace> { <whitespace> <code> <whitespace> }
<inline-space> ::= " " | " " <inline-space>
<optional-inline-space> ::= "" | <inline-space>
<whitespace> ::= <inline-space> | <newline> | <inline-space> <whitespace> | <newline> <whitespace>
<event name> ::= <word> | <word> " " <event name>
<word> ::= "" | <word> <letter>
<letter> ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
<code> ::= <whitespace> <statement> | <whitespace> <statement> <newline> <code>
<newline> ::= "\n"
<statement> ::= <variable assignment> | <variable reassignment> | <control structure> | <send event> | "do nothing"
<variable assignment> ::= <variable name> <inline-space> "is" <inline-space> <expression>
<variable reassignment> ::= <variable name> <inline-space> "is now" <inline-space> <expression>
<control structure> ::= "if" <inline-space> <variable> <inline-space> "{" <newline> <code> <newline> "}" <newline> "else {" <newline> <code> <newline> "}"
<expression> ::= <variable-or-literal> <operator> <variable-or-literal>
<variable-or-literal> ::= <variable> | <literal>
<variable> ::= <word> | <word> "-" <variable>
<literal> ::= <non-whitespace> | <non-whitespace> <literal>
<non-whitespace> ::= «any non space symbol»
<send event> ::= "send" <inline-whitespace> <event name> <inline-whitespace> "[" <inline-whitespace> <argument list> <inline-whitespace> "]" <scheduled time>
<scheduled time> ::= "now" | "in" <inline-whitespace> <variable-or-literal>
<parameter list> ::= "" | <parameter name> ":" <optional-inline-space> <parameter list>
<argument list> ::= "" | <parameter name> ":" <parameter value> <optional-inline-space> <parameter value>
<parameter name> ::= <word> | <word> "-" <parameter name>
<parameter value> ::= <variable-or-literal>
Referencing the tokens in the BNF above:
Time
valuedirection is 45°
) may only occur when declaring a variable the first time in a scopedirection is now 33°
) may not occur to declare a variable, but must occur after a variable has been declared with is
Timeline's standard library consists of events and types. In most cases, events are designed to either be triggered by your code or handled by it, but not both.
Events are the lifeblood of your flight plan, and all your code revolves around responding to events (or handling them), or sending events (or triggering).
These are events you respond to in your flight plan. You would typically not fire these events. Future versions of Timeline may prevent you from doing so.
[
]
The first event triggered. This is where you should set up your trip details.
Triggered if a ping detects an object ahead of the traveler, where a collision will happen in distance time.
The object is at position inside the vortex and a box of size dimensions would enclose the object.
[
]
Triggered when the traveler enters the vortex and the trip begins. Any distances-as-time would start when this is triggered. This is a good place to set up any pre-determined vortex adjustments, or to send pings.
[
]
Triggered when the vortex has been successfully established. This can be used for setup after the connection has been made, however you are more likely to want to handle the traveler entered event, since that is triggered when the traveler actually enters.
These are events you trigger to affect the outcome of your flight plan. You should not write handlers for them. Future versions of Timeline may prevent you from doing so.
[
direction: Angle
]
Manipulates the vortex along the given direction.
[
speed: Percent
]
Manipulates the vortex to slow the traveler down, or speed it up, by the given percentage.
[
]
Send a ping ahead of the traveler. If anything is detected, various events will be triggered. collision imminent is the most common.
[
destination: Destination
traveler: Traveler
]
Contains the details of your trip, including where to go, when to go, and the size of the traveler. When this is handled by the Time-Space Manifold, it will trigger the vortex established event (unless a hardware problem prevents it).
These are the data types provided by the language runtime. Currently you cannot create your own data types, so these are somewhat magical.
%{degrees}°
e.g. 45°
%{degrees}°%{minutes}'
e.g. 45°12'
%{degrees}°%{minutes}'%{seconds}"
e.g. 45°12'34"
Angle - Angle → Angle
Angle + Angle → Angle
An angle in degrees
A destination in time and space—where you want to go.
A cube that describes the outer-most dimensions of an object.
[
kilometers: Integer
]
%{kilometers}km
e.g. 12km
%{meters}m
e.g. 12,345m
Distance + Distance → Distance
Distance - Distance → Distance
A physical distance.
[
]
%{value}
e.g. 123,456,789
Any integer. Generally you won't use this on its own, but use a type literal or a constructor.
[
percentage: Integer
]
%{percentage}%
e.g. 34%
A percentage as a whole number, so e.g. 90% is 0.90.
[
milliseconds: Integer
]
%{milliseconds}ms
e.g. 150ms
An amount of time in seconds.
What is going to travel in time. It has a weight and a cube that describes it's largest dimensions.
[
kilograms: Integer
]
%{kilograms}kg
e.g. 12kg
A weight or mass
The simulator is where you spend most of your time. The simulator will run a Monte Carlo simulation on your flight plan and tell you the risks involved in taking the trip, based on the plan.
> timeline --help SYNOPSIS timeline [--black-box-for=<failure mode>] \ [--details=<failure mode>] \ [--seed=<seed value>] \ <timeline file> DESCRIPTION Simulate the execution of a flight plan. Your typical workflow will be to simulate your plan, examine the risks output by this simulator and then either write code to mitigate those risks (and repeat), or to accept the risks and take the trip, or to abandon the trip altogether. This is called "Risk Driven Development" or RDD and is the primary way in which Timeline code is written OPTIONS <timeline file> The file containing your flight plan. --black-box-for=<failure mode> Re-run the simulation, producing a black box for a sample run in which <failure mode> occurred. This can be helpful for diagnosing very specific failures and mitigating risks too specific for a summary. Often, the use of --details will suggest this option as details are only limited to broader failures. --details=<failure mode> Re-run the simulation showing more details about the given <failure mode>. This is useful if you want to see more ways in which a failure mode manifests itself to mitigate more specific risks. Often used with --seed to recreate a previous run --seed=<seed value> Re-use <seed value> for the random seed to recreate exactly a previous simulated run. EXAMPLES o Run a flight plan $ timeline plan.timeline o Re-Run a flight plan $ timeline --seed=9474837473738 plan.timeline o Figure out what known object you are colliding with $ timeline --details="Known Object Collision" \ plan.timeline o Get a black box for radioactive interference $ timeline \ --black-box-for="Radioactive Interference" \ plan.timeline