Basic Time Travel is an unstructured esoteric programming language based on BASIC that allows time travel. Its only way of making loops involves making the program travel to the past to get its past self to run the same statements again.
Why am I suddenly thinking about spaghetti?
A program is a sequence of statements, with one statement per line. With a few exceptions, each statement starts with a line number, like so:
10 print "Hello, world!"
20 x = 2 + 3
30 print x
Line numbers do not have to be contiguous, but they do have to be ascending, can't repeat, and must be non-negative integers.
Variable names can contain letters, digits, underscores, apostrophes, and any non-ASCII character, but they can't start with a digit or an apostrophe. No words are reserved; whether a word is a keyword or a variable name is determined by context. All variable names and keywords is case-insensitive, with the exception that the case of the first letter of a variable's name determines whether it's global or local (more on that later). (This means that foo
and Foo
are distinct variables, but Foo
and FOO
are the same, as are foo
and fOo
.)
Variables can be either integers or strings, but strings have limited support. String variables have names starting with a $
. All integer variables start out with the value 0, and string variables with the empty string.
If a line starts with the keyword rem
(without a line number), then it's a comment, unless rem
is immediately followed by =
, -
, or +
.
Basic Time Travel has three types of integer literals, any of which can be used anywhere a number is expected (including as line numbers):
123
)
$1F
)
'A
is equivalent to 65
).
An expression is an optional unary operator followed by a literal, a variable name, or an @
sign (which gives the current global time, explained later). Possible unary operators are +
(which usually does nothing) and -
(which negates the value). (There's also another unary operator, \
, but that's only allowed in print
and input
statements and will be explained later.)
The range of integers is implementation-dependent. In the implementation on this page, numbers will be bigints if your browser supports bigints; otherwise the minimum/maximum value is ±9007199254740991. Exceeding the range is an error and will destroy all threads.
Assignment statements have one of the following forms:
line-num var = expr
line-num var = expr1 op expr2
line-num var op expr
The first two forms do what you'd expect. That last form will perform the calculation var op expr
and store the result into var (equivalent to var = var op expr
).
Possible ops are +
, -
, *
, /
, %
(modulo, always positive), and ^
(power). All operations floor their result to an integer, and division by zero causes a black hole that stops all threads.
Note that these operators are part of the syntax of the assignment statement; they can't be used in arbitrary expressions, and multiple binary operators can't be combined in the same statement.
Constants can be defined with a statement without a line number of the form var = expr
. expr can only include literals and constants that have already been defined. Names of constants are completely case-insensitive; that is, foo
and Foo
refer to the same constant, if a constant with either of those names has been defined.
Constants can be used anywhere variables can (except on the left-hand side of an assignment). Additionally, line numbers can have the forms constant + literal
or constant - literal
, which do what you'd expect (thought only those forms or just a plain literal; in particular, a plain constant is not a valid line number). The normal restrictions on line numbers (must be ascending and non-negative, can't be duplicates) apply here, too.
if
statementsAn if
statement has the form line-num if expr1 compare expr2 statement
. compare is any one or more of <
, =
, or >
; if more than one comparison operator is given, the statement runs if any of the operators apply (so e.g. <=
is less than or equals, and <>
is not equals, and =<
and ><
work as well). The statement must be a single statement (with its line number removed); you can't have a block of statements, but you can nest if statements (which acts as an "and" conditional).
Time travel uses a model where the program can go back and change its past. If a program goes back in time, then the program and its past self will be around at the same time, in separate threads; these threads can then interact with each other.
There's a global timer that starts at 0, and each thread has a local timer that also starts at 0 and goes at the same speed as the global timer. By default, these don't correspond to real time, they just determine the ordering of events in the program. The line number at the beginning of each line determines at what time on the local timer the statement executes. You can find the current value of the global timer by using @
anywhere an expression is expected (there's no way to get the value of the local timer, since you already know that from the line number).
Variables can be either local or global. Global variables (whose names start with a capital letter) are shared by all threads, and allow communication between threads. For local variables (whose names start with a lowercase letter), each thread has its own copy of the variable, and the thread remembers them when it travels through time.
goto
The most important time travel command is goto
, which causes the thread to travel to a particular point in time. Its most basic form is line-number goto expression
; this will cause the thread to travel to the global time mentioned in expression. expression can be preceded by @
, in which case it's treated as relative to the current global time. Also, between goto
and the expression (or @
), you can put a symbol specifying what order the thread should run relative to other threads:
{ | The thread should run first, before any threads that already exist |
< | The thread should run immediately before its current incarnation (the default) |
> | The thread should run immediately after its current incarnation |
} | The thread should run last, after any threads that already exist |
? | The thread should run in a random place in the sequence |
If the expression has a unary operator and the goto
isn't relative, then a thread order indicator must be specified to distinguish the statement from a statement that adds to or subtracts from the variable goto
.
If the time specified is after the present, then the interpreter will continue running other threads normally, and start running the current thread again when it arrives. If the time specified is before the present, then the interpreter will roll back everything, including global variables, local variables (of threads other than the one that's currently time traveling), and any output (so going back to the start of the program clears the screen), to how it was at that point in global time. However, if a thread goes to a particular time, it will always arrive at that time, each time the program gets to that time, no matter what else happens.
Here's an example, "Count", which counts up forever, each time erasing the previous number and replacing it:
1000 slow
1001 Count = 0
1002 count = Count
1003 print count
2000 goto } 1000
2001 Count = count + 1
slow
will be explained later, but it makes it so the numbers stay long enough to be visible.
Count
and count
are set to 0, and 0 is printed.
Count = 0
, but then the new thread (at local time 2001) runs Count = count + 1
, which sets the global variable Count
to 1. Then lines 1002 and 1003 end up seeing 1 as the count and printing that.
count
as 1, so at global time 1001/local time 2001, it sets Count
to 2, and since this new thread runs last (due to }
), the original thread sees Count
as 2, prints that, and goes back in time again.
stop
and start
In addition to traveling through time, threads can also stop time. If a thread runs the command line-number stop
, then other threads will stop running, global time will stop increasing, local time for other threads will stop increasing, but the current thread will continue running and local time will continue increasing for it. This lasts until a command of the form line-number start
is executed; goto
, freeze
, and leave
will also cancel this effect.
freeze
and thaw
The command line-number freeze
cryonically freezes the current thread. The current thread will stop running, but other threads will continue as normal. line-number thaw
wakes up all frozen threads. When a thread thaws, its local time will be the same as when it's frozen.
This is similar to a goto
with a time in the future, but it's useful if you don't yet know the time you're going to. It also differs from goto
in that goto some time in the future
will cause the thread to always arrive at that time no matter what, whereas if a thread is thaw
ed on one loop but not on another, it'll only thaw on the loop where it's thaw
ed.
There are two ways that you can synchronize global time within the program with the actual real-world time.
The first is slow mode. If a thread runs the command line-number slow
, then after that, each unit of global time will take 1 millisecond. This means, for instance, that if a command is executed at global time 500, and the next command is executed at global time 600, the interpreter will wait 100 milliseconds (1/10 of a second) between executing those commands. No attempt is made to account for the time it takes to actually do the relevant calculations, so this could get out of sync with real time.
You can end slow mode with line-number fast
, and you can make the program start in slow mode by having a line that's just slow
without a line number. Slow mode state acts like a global variable; if you travel back in time, then the slow mode state will be whatever it was at that time. A difference between putting 0 slow
at the beginning of the program and using line-number-less slow
is that in the former case, going back before time 0 will result in slow mode being disabled, whereas it won't using the line-number-less version.
The second way to synchronize with real time is the set
command (line-number set
). Running the set
command will set the clock used for global time to be the number of milliseconds since January 1, 1970 (not counting leap seconds). This doesn't change the relative ordering of events; rather, it adds an offset to any @
expression, and subtracts that offset from any goto
command. The offset is treated like a global variable; going back before the set
command will restore the offset to 0. (This means that the value of @
after a goto
command might not be the argument that you gave to the command.)
In addition, set
sets the global variable Z
to the current time zone offset, in number of milliseconds east of UTC. This means that @+Z
gives the current local time.
leave
The command line-number leave
causes the current thread to exit. (Note that if the program travels back in time before the leave
command, the thread will still run.)
print
and input
The main way to output stuff is with the print
statement. The command print
is followed by zero or more strings and expressions, separated by spaces; the items are concatenated together and shown on the screen. By default, a newline is printed after the statement; ending the statement with ;
disables this.
A string is any text (without newlines) within double quotes. Backslash escapes are not supported, but you can put two double quotes next to each other to include a quotation mark in the output. Also, an additional operator, \
, is supported in print
statements; this interprets its operand as a Unicode character code and prints the corresponding character.
print
can't be used with a single expression that has a unary +
or -
, because that would be interpreted as adding to or subtracting from a variable called print
. You can get around this limitation by outputting multiple expressions in a line (perhaps an empty string).
The main way to input stuff is with the input
statement. It has the same syntax as print
. Any expression that's just a variable will be shown as a text box where the user can enter a number, which will then be stored in the variable; anything else (including a variable with a +
before it) is just printed. All threads and the global time will pause as the program waits for input. An input
statement that doesn't mention any variables is allowed; in that case, it just prints any expressions and strings and waits for the user to click "OK".
It's also possible to input strings, by putting the name of a string variable in an input
statement. String variables have names that start with $
; the first letter after the dollars sign determines whether it's global or local, and string variables work the same as integer variables in terms of time travel. Integer variable names can't start with decimal digits; since $
is also used for hexadecimal literals, string variable names can't start with hexadecimal digits.
Input statements are the only way to get a string into a string variable. There is no assignment statement for strings.
Once a string is in a variable, there are a few ways to use it:
print
command.
line-number if $variable string-literal statement
(note the lack of equals sign). This compares case-insensitively.
line-number variable < $variable
; this removes the first character from the string and puts the Unicode code of that character into variable.
Basic Time Travel has some limited audio output capabilities, using the chime
command. Chimes will only be audible if the program is in slow mode.
chime
takes a single argument, which can either be a note name (c
through b
or C
through B
) or a variable (not an arbitrary expression) containing a note number. There are two octaves of chimes, from C to C; the lower octave is indicated by lowercase letters or numbers from 1 to 7, and the higher octave is indicated by uppercase letters or numbers from 8 to 15. Note that the high C is not available directly as a note name (but you can use B♯), but is available as a note number. Also note that the note numbers are diatonic, not chromatic (1 is C, 2 is D, etc., and there's no number for C♯ etc.).
In addition, there are two ways to specify accidentals. One is to follow the note name or variable by a number sign (#
), which will raise the note one half step. The other is to set a local variable named key
; if this variable has a positive value, then it uses a key signature with that many sharps, and if it's negative, then it uses that many flats (e.g. key = 1
means that all F's, whether specified as the name f
or F
or a variable with the value 4 or 11, will be sharp). Both of these combine, so e.g. if key
is 1, chime F#
will give F double sharp (G), and if key
is -1, chime B#
will give B natural. If an accidental puts the note outside the two-octave range, then the note won't play.
Note that the chimes have non-harmonic overtones, so things like octaves played at the same time won't sound as good as they would on most instruments.
number | → | digit+ |
| | $ hex-digit+
| |
| | ' character
| |
string | → | " (character - " | "" )* "
|
var-name | → | any word |
primary | → | number |
| | var-name | |
| | @
| |
expr | → | (+ | - )? primary
|
op | → | + | - | * | / | % | ^
|
io-item | → | expr |
| | \ primary
| |
| | string | |
| | $ var-name
| |
stmt-body | → | var-name < $ var-name
|
| | var-name = expr (op expr)?
| |
| | var-name op expr | |
| | (print | input ) io-item* ; ?
| |
| | goto ({ | < | > | } | ? )? @ ? expr
| |
| | if expr (< | > | = )+ expr stmt-body
| |
| | if $ var-name string stmt-body
| |
| | chime var-name # ?
| |
| | stop | start | freeze | thaw | slow | fast | set | leave
| |
stmt | → | var-name = expr
|
| | number stmt-body | |
| | var-name (+ | - ) number stmt-body
| |
| | slow
| |
| | rem anything
| |
| | (empty line) | |
program | → | list of stmt, separated by newlines |
Photosensitivity warning: Due to how this language handles output, some programs may cause the output to flicker while running.