Basic Time Travel

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?

Contents

General syntax

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 +.

Integers

Basic Time Travel has three types of integer literals, any of which can be used anywhere a number is expected (including as line numbers):

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

Assignment statements have one of the following forms:

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

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 statements

An 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

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
  1. slow will be explained later, but it makes it so the numbers stay long enough to be visible.
  2. The first time through, Count and count are set to 0, and 0 is printed.
  3. At time 2000 (local and global), it goes back to time 1000, which erases the printed number. At this point there are two threads; in the original thread, local and global time are the same, but in the new arrival, global time 1000 corresponds to local time 2000 (so the difference is 1000).
  4. At global time 1001, the original thread runs 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.
  5. Then the original thread goes back in time again; at this point, there are three threads, the two from before and one new one. This new thread remembers the local 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.
  6. The process then repeats forever, adding more and more threads each time.

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 thawed on one loop but not on another, it'll only thaw on the loop where it's thawed.

Synchronizing with real time

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.)

IO

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".

String input

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:

Audio output

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.

Grammar

numberdigit+
|$ hex-digit+
|' character
string" (character - " | "")* "
var-nameany word
primarynumber
|var-name
|@
expr(+ | -)? primary
op+ | - | * | / | % | ^
io-itemexpr
|\ primary
|string
|$ var-name
stmt-bodyvar-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
stmtvar-name = expr
|number stmt-body
|var-name (+ | -) number stmt-body
|slow
|rem anything
|(empty line)
programlist of stmt, separated by newlines

Interpreter

Photosensitivity warning: Due to how this language handles output, some programs may cause the output to flicker while running.