SourceHut | GitHub |
---|
Contents
Download from the latest release
go install github.com/benbusby/y2k@latest
- Install Go: https://go.dev/doc/install
- Clone and build project:
git clone https://github.com/benbusby/y2k.git
cd y2k
go build
- Variable creation
- Supported types:
int
,float
,string
- Supported types:
- Variable modification
- Supported operations:
+=
,-=
,/=
,*=
,**= (exponentiation)
,= (overwrite)
- Accepts primitive types (
int
,float
,string
) or variable IDs as arguments
- Supported operations:
- Conditional logic
- Supported types:
if
,while
- Supported comparisons:
==
,>
,<
, and divisibility (% N == 0
)
- Supported types:
- Print statements
- Supported types:
var
,string
- Supported types:
- Debug mode
- Outputs where/how each timestamp digit is being parsed
- "Raw" file reading/writing
- Allows writing Y2K programs as file content (see Examples) and exporting to a set of new 0-byte files with their timestamps modified, rather than manually editing individual file timestamps.
y2k [args] <input>
Args:
-d int
Set # of digits to parse at a time (default 1)
-debug
Enable to view interpreter steps in console
-export
Export a Y2K raw file to a set of timestamp-only files
-outdir string
Set the output directory for timestamp-only files when exporting a raw Y2K file.
This directory will be created if it does not exist. (default "./y2k-out")
Note: See CHEATSHEET.md for help with writing Y2K commands.
The simple way to write Y2K programs is to write all commands to a file as regular file content first.
For example, from the "Set and Print Variable" program:
# set-and-print-var.y2k
8124 # Create new variable 1 with type int (2) and size 4
1999 # Insert 4 digits (1999) into variable 1
9211 # Print variable 1
$ y2k set-and-print-var.y2k
1999
You can then export this file to a set of empty 0-byte files (or in this example, just one file) with their timestamps modified to achieve the same functionality as the raw file:
$ y2k -export set-and-print-var.y2k
Writing ./y2k-out/0.y2k -- 812415009210000000 (1995-09-29 16:50:09.21 -0600 MDT)
$ ls ./y2k-out/*.y2k -lo --time-style="+%s%9N"
-rw-r--r-- 1 benbusby 0 812415009210000000 ./y2k-out/0.y2k
Then you could pass the new output directory as input to y2k
, and verify that
the program still functions the same, but with completely empty 0-byte files.
$ y2k ./y2k-out
1999
See Examples for more detailed breakdowns of current example programs.
To preface, Y2K is obviously a fairly unconventional language. Since everything is interpreted using numbers, it can perhaps be a bit confusing at first to get a feel for how to write programs. If you find any of the below documentation confusing, please let me know!
Y2K works by reading all files in a specified directory (sorted numerically) and extracting each of their unix nanosecond timestamps. It then concatenates each timestamp, stripping the first digit off of each timestamp except for the first one. This is done to eliminate the potential issue of a command spanning across multiple file timestamps where a 0 might be required at the beginning of the timestamp. For example, if the number 1000 was being written to a variable and the 0s needed to be at the beginning of the next file timestamp, this would only be possible if the timestamp was prefixed with a non-zero digit (otherwise leading 0s are ignored).
After the timestamps have been concatenated into one long string, this string
is passed into the top level interpreter.Parse()
function, which will
interpret the first digit as a command ID in order to determine which action to
take. Command IDs are mapped to fields that are unique to that particular
command, and the interpreter will use the next N-digits to parse out values for
each of those fields. Some commands, such as setting and modifying variables,
have a "Size" field which tells the interpreter how many digits following the
command fields will be used to store/use a specific value. For instance, if you
wanted to store the number 100 in a variable, you would use the "Create
Variable" command ID, and the "Size" field for that command would be 3. The
following 3 digits of the timestamp would be "100", and the interpreter would
then read and store that 3-digit value in the variable.
Once the interpreter finishes reading the command ID, the command fields, and any subsequent N-digit values (if applicable), it returns to the beginning to parse the next command ID.
CHEATSHEET.md contains a simplified breakdown of command IDs, command fields, and when values are needed for the different commands. Please also refer to the following Examples section for simple programs that help to inform how the Y2K interpreter works.
Each example below is taken directly from the examples
folder,
but with added explanation for how/why they work.
All examples can be exported to 0-byte solutions using the -export
flag if
desired.
examples/set-and-print-var.y2k
Timestamp:
812419999211000000 (1995-09-29 18:13:19.211000000)
This example simply sets an integer variable to the value 1999
and then
prints that variable to the console.
8124 # Create new variable 1 with type int (2) and size 4
1999 # Insert 4 digits (1999) into variable 1
9211 # Print variable 1
Output: 1999
examples/modify-and-print-var.y2k
Timestamp:
812419997120149211 (1995-09-29 18:13:17.120149211)
This example expands on the previous example by modifying the variable's
value after creating it. In this case, we're taking the original variable
value (1999
) and subtracting 4
from it to get 1995
.
8124 # Create (8) a variable (1) with type "int" (2) and size 4
1999 # Insert 4 digits (1999) into variable 1
71201 # On variable 1, call function "-=" (2) with a primitive (0) 1-digit argument
4 # Insert 1 digit (4) into function argument
9211 # Print variable 1
Output: 1995
Timestamp(s):
502090112340512121 (1985-11-28 22:28:32.340512121)
850049151812046300 (1996-12-08 05:45:51.812046300)
In this example, we're printing the string "Hello World!". Since character codes are easier to encapsulate with 2-digit codes (and the string we're printing is a 2-digit number), we need to switch the interpreter to 2-digit parsing mode at the very beginning.
5 0 2 # Switch interpreter to 2-digit parsing size
09 01 12 # Begin printing a string with a size of 12
34 05 12 12 15 00 # Write "Hello "
49 15 18 12 04 63 # Write "World!"
Output: Hello World!
Timestamp(s):
813913141592679501 (1995-10-17 00:59:01.592679501)
827131199211000000 (1996-03-17 23:39:59.211000000)
In this example, we're introducing a couple of new concepts. One is the ability to include variables from the command line, and the other is modifying one variable using another variable's value.
To include variable's from the command line, we simply pass the value after the
input. For example, y2k my-program.y2k 10
would include a variable with the
value 10
that we can access from the beginning of the program. Since most Y2K
programs create variables using sequential IDs (i.e. 0 -> 1 -> 2, etc),
variables added from the command line are added to the back of the variable
map, with descending IDs from there. So if you're running Y2K in the default
1-digit parser mode, command line arguments are added as variables with IDs
starting at 9, then 8, and so on. As an example: y2k my-program.y2k foo bar
would have variable 9 set to "foo" and variable 8 set to "bar".
The other new concept is modifying a variable with the value from another variable. In previous examples, we've used primitive types for arguments, but in this case we need to multiply our "Pi" variable (1) by our squared radius. To do this, we set the third field to "1" to tell the interpreter that the value we're passing in is a variable ID, not a primitive type.
8139 # Set variable 1 to type float (3) and size 9
131415926 # Insert 9 digits (131415926) into variable 1, using the first
# digit (1) as the decimal placement (3.1415926)
79501 # Modify variable 9 (CLI arg) using the "**=" function (5),
# with a non-variable (0) argument size of 1
2 # Use the number 2 as the function argument (var9 **= 2)
71311 # Modify variable 1 using the "*=" function (3), with a
# variable argument (1) with a variable ID size of 1
9 # Use the variable ID 9 in the function argument (var1 *= var9)
9211 # Print variable 1
Output (y2k examples/area-of-circle.y2k 10
):
314.15926
Output (y2k examples/area-of-circle.y2k 25
):
1963.495375
examples/fibonacci-n-terms.y2k
Timestamp(s):
812108221183210693 (1995-09-26 03:37:01.183210693)
811092117391117191 (1995-09-14 09:21:57.391117191)
812721113792011000 (1995-10-03 05:51:53.792011000)
For this modification to the Fibonacci Sequence program, we're now using an argument from the command line as the number of terms to print. In this new program, we'll take the command line argument and create a new loop that decrements that value until it reaches 0.
We need 3 variables for this program, not including the variable added from the command line: a variable for the "current" value, a "placeholder" variable to track the "current" value before it gets updated, and a "next" variable to track the "next" value in the sequence. On each loop iteration, we 1) print "current", 2) set "placeholder" to "current", 3) set "current" to "next", 4) add "placeholder" to "next", and 5) decrement counter.
8121 # Create variable 1 with type int (2) and size 1
0 # Insert 1 digit (0) into variable 1
8221 # Create variable 2 with type int (2) and size 1
1 # Insert 1 digit (1) into variable 2
8321 # Create variable 3 with type int (2) and size 1
0 # Insert 1 digit (0) into variable 3
# Init while loop (while var 9 > 0)
69311 # Create conditional using variable 9, with comparison ">" (3),
# as a loop (1), and with a right hand value size of 1
0 # Insert 1 digit (0) into conditional's right hand value
# Begin while loop
9211 # Print var 9
739111 # var 3 = var 1
719112 # var 1 = var 2
721113 # var 2 += var 3
792011 # var 9 -= 1
Output 1 (y2k examples/fibonacci-n-terms.y2k 15
):
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
Output 2 (y2k examples/fibonacci-n-terms.y2k 20
):
0
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
Timestamp(s):
502080901043209262 (1985-11-28 19:55:01.043209262)
860808010428212626 (1997-04-11 19:20:10.428212626)
805000187919771118 (1995-07-05 21:09:47.919771118)
861213100711011614 (1997-04-16 11:51:40.711011614)
802159217420006140 (1995-06-03 00:00:17.420006140)
813921942000614015 (1995-10-17 03:25:42.000614015)
892184200092110000 (1998-04-09 22:56:40.092110000)
The Fizz Buzz program highlights a few features that haven't been covered yet, namely terminating and "continue"-ing conditionals. We also have to tell the interpreter to switch between 1- and 2-digit parsing in order to create our words "fizz" and "buzz" while maintaining the efficiency of 1-digit parsing.
The value 2000
, when used within a non-looped conditional, tells the
interpreter where the "body" of the statement needs to end. This is an
arbitrarily chosen value (but fits with the name of the language) that is used
multiple times in this program to tell the interpreter where an "if" statement
ends. There's also the new command ID 4
(aka CONTINUE
), which returns an
empty string to the parent parser function instead of the remainder of the
timestamp. Since this is being used inside a "while" loop, this returns the
interpreter back to the beginning of the loop to reevaluate instead of
continuing to the next part of the timestamp.
502 # Change interpreter to 2-digit parsing mode
# Set variables 9 and 8 to "fizz" and "buzz" respectively
08 09 01 04 # Create variable 9 with type string (1) and length 4
32 09 26 26 # Insert 4 chars ("Fizz") into variable 9
08 08 01 04 # Create variable 8 with type string (1) and length 4
28 21 26 26 # Insert 4 chars ("Buzz") into variable 8
05 00 01 # Change interpreter back to 1-digit parsing mode
# Set variable 7 to "fizzbuzz"
8791 # Create variable 7 with type "copy" (9) and length 1 (variable ID length)
9 # Use 1 digit variable ID (9) to copy values from var 9 to var 7
77111 # On variable 7, call function "+=" (5) using a variable (1) with a 1 digit ID
8 # Use 1 digit variable ID (8) to append values from var 8 to var 7
# Begin the loop from 0 to 100
61213100 # while variable 1 < 100 (implicit creation of var 1)
711011 # var 1 += 1
6140215 # if var 1 % 15 == 0
9217 # print var 7 ("fizzbuzz")
4 # continue
2000 # end-if
614013 # if var 1 % 3 == 0
9219 # print var 9 ("fizz")
4 # continue
2000 # end-if
614015 # if var 1 % 5 == 0
9218 # print var 8 ("buzz")
4 # continue
2000 # end-if
9211 # print var 1
Output:
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
17
fizz
19
buzz
fizz
22
23
fizz
buzz
26
fizz
28
29
fizzbuzz
...<continued>...
Timestamp(s):
611110721011921200 (1989-05-13 18:58:41.011921200)
Originally from this problem on codegolf.stackexchange.com.
You may have noticed something "sneaky" in the Fizz Buzz example, which is the
implicit creation of variables when they're referenced without being explicitly
created beforehand. This is somewhat similar to the for i in val
behavior
seen in other languages, where i
is created from the context it's used in.
Currently, Y2K only supports initialization of integer variables with a value
of 0
, but this could be expanded in future versions to support iterating over
strings, arrays, and so on.
This program -- a simple program to count up by 1 until killed -- highlights that feature. In this example, we create variable 1 through its reference in the while loop, and variable 2 the first time that we try to modify it.
Creating variables this way isn't necessarily recommended, since it makes programs more difficult to read and, as previously mentioned, can only be used for creating variables with a value of 0. But it can be a useful way to condense a solution into an even smaller footprint. In this case, we can fit the solution to the problem in a single file timestamp (and in raw format is only 15 bytes after comments and newlines are removed).
611110 # while var 1 == 0
721011 # var 2 += 1
9212 # print var 2
Output:
1
2
3
4
5
...<continued until killed>...
- Why the pre-2000 timestamp limitation? Why the name Y2K?
The language was originally designed to interpret timestamps of any length, but
both macOS and Go store Unix nanoseconds as an int64. The max value of an int64
has 19 digits (9223372036854775807
) but it wouldn't be reliable to write
programs using all 19 digits, since there can be programs that exceed this
value fairly easily (a program to print the letter 'c' would start with
923...
, for example). As a result, all timestamps for Y2K programs have 18
digits, which results in a maximum timestamp that falls around the year 2000¹.
The interpreter was also originally designed to only ever read 2 digits at a time. These combined limitations reminded me of the "Y2K Problem", hence the name.
- What does 0-byte actually mean? How can a program be 0 bytes?
Since the interpreter only reads file timestamps and not file content, each
.y2k
file can be completely empty (0 bytes) without affecting how each
program is interpreted. And since every file has to have a timestamp associated
with it anyway, there aren't any extra bytes needed to achieve this
functionality. Technically though, there's no such thing as a 0 byte file --
the metadata for that file does have to be stored somewhere. But for code
golfing purposes, I believe it would be counted as 0 bytes.
- Why are there two ways to copy a variable's value to a new variable?
The method through the SET
command (8
) inserts a new reference to a
variable using the specified ID, whereas the method through the MODIFY
command (7
) updates the existing reference in the variable table. The former
can be useful for instantiating a new variable from an existing one, but can
cause problems if you're within the scope of a condition that has referenced
that variable.
For example:
81210 # int var 1 = 0
82210 # int var 2 = 0
# BAD
# Loops infinitely, since the reference to Var 1 that
# was used to create the loop is overwritten, and the
# value of the original reference is never updated
61213100 # While Var 1 < 100
721101 # Var 2 += 1
81912 # Overwrite Var 1 with Var 2 values
# GOOD
# Loops as expected, Var 1's value is updated on each
# iteration with Var 2's value
61213100 # While Var 1 < 100
721101 # Var 2 += 1
719112 # Copy Var 2 value to Var 1
- How would I show proof of my solution in a code golf submission?
I'm not sure the best way to do this yet. Assuming you wrote your solution
as a "raw" Y2K file, you can run y2k -export my-program.y2k
, and then
run the following command:
$ ls ./y2k-out/*.y2k -lo --time-style="+%s%9N"
-rw-r--r-- 1 benbusby 0 502090134051212150 0.y2k
-rw-r--r-- 1 benbusby 0 104915181204630000 1.y2k
You could also include your raw Y2K file contents along with the 0-byte proof, to be extra thorough.
- Why doesn't Y2K have X feature?
The language is still in development. Feel free to open an issue, or refer to the Contributing section if you'd like to help out!
¹ Technically Sept. 2001, but close enough...
I would appreciate any input/contributions from anyone. Y2K still needs a lot of work, so feel free to submit a PR for a new feature, browse the issues tab to see if there's anything that you're interested in working on, or add a new example program.
The main thing that would help is trying to solve current or past code-golfing problems from https://codegolf.stackexchange.com. If there's a limitation in Y2K (there are definitely a ton) that prevents you from solving the problem, open an issue or PR so that it can be addressed!