├── .build.yml
├── .gitignore
├── CHEATSHEET.md
├── LICENSE
├── README.md
├── cross-compile.sh
├── examples
├── area-of-circle.y2k
├── count-up-forever.y2k
├── fibonacci-n-terms.y2k
├── fizz-buzz.y2k
├── hello-world.y2k
├── modify-and-print-var.y2k
└── set-and-print-var.y2k
├── go.mod
├── main.go
├── src
├── interpreter
│ ├── condition.go
│ ├── interpreter.go
│ ├── modifier.go
│ ├── print.go
│ └── variable.go
└── utils
│ ├── raw.go
│ └── utils.go
└── test.sh
/.build.yml:
--------------------------------------------------------------------------------
1 | image: archlinux
2 | packages:
3 | - go
4 | sources:
5 | - https://git.sr.ht/~benbusby/y2k
6 | tasks:
7 | - test: |
8 | cd y2k
9 | ./test.sh
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Unix timestamps aren't preserved in git, so no point in storing
2 | # y2k files
3 | [0-9]*.y2k
4 | y2k
5 | y2k-out
6 |
7 | .idea
8 | out
9 |
--------------------------------------------------------------------------------
/CHEATSHEET.md:
--------------------------------------------------------------------------------
1 | # Y2K Command Cheat Sheet
2 |
3 | This provides a quick rundown of how to perform various commands.
4 |
5 | All commands follow the same basic flow:
6 |
7 | Command ID -> Command Fields -> Command Value
8 |
9 | ## Command IDs
10 |
11 | Every Y2K command should start by referencing a command ID. If you wanted
12 | to print something, your command would start with `9`, if you wanted to
13 | create a new variable, your command would start with `8`, and so on.
14 |
15 |
16 |
17 |
Command
18 |
Description
19 |
ID
20 |
21 |
22 |
PRINT
23 |
Print variable or string
24 |
9
25 |
26 |
27 |
CREATE
28 |
Create a new variable
29 |
8
30 |
31 |
32 |
MODIFY
33 |
Modify an existing variable
34 |
7
35 |
36 |
37 |
CONDITION
38 |
Create a condition
39 |
6
40 |
41 |
42 |
META
43 |
Modify interpreter state
44 |
5
45 |
46 |
47 |
CONTINUE
48 |
Continue (in loop)
49 |
4
50 |
51 |
52 |
53 | ## Command Fields
54 |
55 | After the command ID, the next N digits should complete the required fields
56 | for the specified command. If a command has 3 fields, then the next 3 digits
57 | would be assigned to those fields, for example.
58 |
59 |
60 |
61 |
Command ID
62 |
Fields
63 |
64 |
65 |
9 (PRINT)
66 |
67 |
68 |
Type
69 |
70 |
1 --> String
71 |
2 --> Variable
72 |
73 |
Size
74 |
75 |
76 |
77 |
78 |
8 (CREATE)
79 |
80 |
81 |
Variable ID
82 |
Type
83 |
84 |
1 --> String
85 |
2 --> Integer
86 |
3 --> Float
87 |
88 |
Size should be # digits + 1, with the first digit used for decimal placement.
89 |
Example: 3.14 would require Size = 4, with the first digit set to 1 (1314).
90 |
91 |
9 --> Copy
92 |
93 |
Size
94 |
95 |
96 |
97 |
98 |
7 (MODIFY)
99 |
100 |
101 |
Variable ID
102 |
Function
103 |
104 |
1 --> +=
105 |
2 --> -=
106 |
3 --> *=
107 |
4 --> /=
108 |
5 --> **= (exponentiation)
109 |
9 --> =
110 |
111 |
Argument Is Variable
112 |
113 |
0 --> No
114 |
1 --> Yes (value will be treated as a variable ID)
115 |
116 |
Argument Size
117 |
118 |
119 |
120 |
121 |
6 (CONDITION)
122 |
123 |
124 |
Variable ID
125 |
Comparison
126 |
127 |
1 --> ==
128 |
2 --> <
129 |
3 --> >
130 |
4 --> Is evenly divisible by
131 |
132 |
Loop
133 |
134 |
0 --> if
135 |
1 --> while
136 |
137 |
Argument Size
138 |
139 |
140 |
141 |
142 |
5 (META)
143 |
144 |
145 |
Debug Mode
146 |
147 |
0 --> Off
148 |
1 --> On
149 |
150 |
# of digits
151 |
152 |
Updates the number of digits parsed on each pass of the interpreter
153 |
154 |
155 |
156 |
157 |
158 |
159 | ## Command Value
160 |
161 | After the command ID and fields are set, the next step is to read in a value
162 | that matches the "Size" field (if applicable). For example, if you're creating
163 | a new variable, and the variable size is set to `4`, the next 4 digits would
164 | contain the variable's value. If you're modifying an existing variable, and the
165 | argument size is `3`, the next 3 digits would contain the argument value for the
166 | modifier function.
167 |
168 | `PRINT` statements don't have a size field. Depending on the `Type` specified,
169 | a print statement either:
170 |
171 | - `String`: Converts the following digits into characters until a 2-space
172 | sequence is reached. See [Character Codes](#character-codes) for help.
173 | - `Variable`: Use the next digit as a variable ID, and prints that variable
174 |
175 | Once the command value is set, the interpreter returns to the start and looks for
176 | the next command ID to repeat this process over again.
177 |
178 | ### Character Codes
179 |
180 | #### Alphabet
181 |
182 |
476 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
21 |
22 | Contents
23 | 1. [Install](#install)
24 | 2. [Features](#features)
25 | 3. [Usage](#usage)
26 | 4. [How It Works](#how-it-works)
27 | 5. [Examples](#examples)
28 | 1. [Set and Print Variable](#set-and-print-variable)
29 | 2. [Modify Variable](#modify-and-print-variable)
30 | 3. [Print "Hello World!"](#hello-world)
31 | 4. [Area of a Circle](#area-of-a-circle)
32 | 5. [Fibonacci Sequence (N-terms)](#fibonacci-sequence)
33 | 6. [Fizz Buzz](#fizz-buzz)
34 | 7. [Count Up Forever (Golf Hack)](#count-up-forever)
35 | 6. [FAQ](#faq)
36 | 1. [Why the pre-2000 timestamp limitation? Why the name Y2K?](#faq)
37 | 2. [What does 0-byte actually mean? How can a program be 0 bytes?](#faq)
38 | 3. [Why are there two ways to copy a variable's value to a new variable?](#faq)
39 | 4. [How would I show proof of my solution in a code golf submission?](#faq)
40 | 5. [Why doesn't Y2K have X feature?](#faq)
41 | 7. [Contributing](#contributing)
42 |
43 | ## Install
44 |
45 | ### Binary (Windows, macOS, Linux)
46 | Download from [the latest release](https://github.com/benbusby/y2k/releases)
47 |
48 | ### Go
49 | `go install github.com/benbusby/y2k@latest`
50 |
51 | ### From Source
52 |
53 | 1. Install Go: https://go.dev/doc/install
54 | 2. Clone and build project:
55 | ```
56 | git clone https://github.com/benbusby/y2k.git
57 | cd y2k
58 | go build
59 | ```
60 |
61 | ## Features
62 |
63 | - Variable creation
64 | - Supported types: `int`, `float`, `string`
65 | - Variable modification
66 | - Supported operations: `+=`, `-=`, `/=`, `*=`, `**= (exponentiation)`, `= (overwrite)`
67 | - Accepts primitive types (`int`, `float`, `string`) or variable IDs as arguments
68 | - Conditional logic
69 | - Supported types: `if`, `while`
70 | - Supported comparisons: `==`, `>`, `<`, and divisibility (`% N == 0`)
71 | - Print statements
72 | - Supported types: `var`, `string`
73 | - Debug mode
74 | - Outputs where/how each timestamp digit is being parsed
75 | - "Raw" file reading/writing
76 | - Allows writing Y2K programs as file content (see [Examples](#examples)) and
77 | exporting to a set of new 0-byte files with their timestamps modified,
78 | rather than manually editing individual file timestamps.
79 |
80 | ## Usage
81 |
82 | ```
83 | y2k [args]
84 |
85 | Args:
86 | -d int
87 | Set # of digits to parse at a time (default 1)
88 | -debug
89 | Enable to view interpreter steps in console
90 | -export
91 | Export a Y2K raw file to a set of timestamp-only files
92 | -outdir string
93 | Set the output directory for timestamp-only files when exporting a raw Y2K file.
94 | This directory will be created if it does not exist. (default "./y2k-out")
95 | ```
96 |
97 | ____
98 |
99 | ***Note:** See [CHEATSHEET.md](CHEATSHEET.md) for help with writing Y2K commands.*
100 |
101 | The simple way to write Y2K programs is to write all commands to a file as
102 | regular file content first.
103 |
104 | For example, from [the "Set and Print Variable" program](#set-and-print-variable):
105 |
106 | ```elixir
107 | # set-and-print-var.y2k
108 | 8124 # Create new variable 1 with type int (2) and size 4
109 | 1999 # Insert 4 digits (1999) into variable 1
110 |
111 | 9211 # Print variable 1
112 | ```
113 |
114 | ```shell
115 | $ y2k set-and-print-var.y2k
116 | 1999
117 | ```
118 |
119 | You can then export this file to a set of empty 0-byte files (or in this
120 | example, just one file) with their timestamps modified to achieve the same
121 | functionality as the raw file:
122 |
123 | ```shell
124 | $ y2k -export set-and-print-var.y2k
125 | Writing ./y2k-out/0.y2k -- 812415009210000000 (1995-09-29 16:50:09.21 -0600 MDT)
126 |
127 | $ ls ./y2k-out/*.y2k -lo --time-style="+%s%9N"
128 | -rw-r--r-- 1 benbusby 0 812415009210000000 ./y2k-out/0.y2k
129 | ```
130 |
131 | Then you could pass the new output directory as input to `y2k`, and verify that
132 | the program still functions the same, but with completely empty 0-byte files.
133 |
134 | ```shell
135 | $ y2k ./y2k-out
136 | 1999
137 | ```
138 |
139 | See [Examples](#examples) for more detailed breakdowns of current example programs.
140 |
141 | ## How It Works
142 |
143 | To preface, Y2K is obviously a fairly unconventional language. Since everything
144 | is interpreted using numbers, it can perhaps be a bit confusing at first to get
145 | a feel for how to write programs. If you find any of the below documentation
146 | confusing, please let me know!
147 |
148 | Y2K works by reading all files in a specified directory (sorted numerically)
149 | and extracting each of their unix nanosecond timestamps. It then concatenates
150 | each timestamp, stripping the first digit off of each timestamp except for the
151 | first one. This is done to eliminate the potential issue of a command spanning
152 | across multiple file timestamps where a 0 might be required at the beginning of
153 | the timestamp. For example, if the number 1000 was being written to a variable
154 | and the 0s needed to be at the beginning of the next file timestamp, this would
155 | only be possible if the timestamp was prefixed with a non-zero digit (otherwise
156 | leading 0s are ignored).
157 |
158 | After the timestamps have been concatenated into one long string, this string
159 | is passed into the top level `interpreter.Parse()` function, which will
160 | interpret the first digit as a command ID in order to determine which action to
161 | take. Command IDs are mapped to fields that are unique to that particular
162 | command, and the interpreter will use the next N-digits to parse out values for
163 | each of those fields. Some commands, such as setting and modifying variables,
164 | have a "Size" field which tells the interpreter how many digits following the
165 | command fields will be used to store/use a specific value. For instance, if you
166 | wanted to store the number 100 in a variable, you would use the "Create
167 | Variable" command ID, and the "Size" field for that command would be 3. The
168 | following 3 digits of the timestamp would be "100", and the interpreter would
169 | then read and store that 3-digit value in the variable.
170 |
171 | Once the interpreter finishes reading the command ID, the command fields, and
172 | any subsequent N-digit values (if applicable), it returns to the beginning to
173 | parse the next command ID.
174 |
175 | [CHEATSHEET.md](CHEATSHEET.md) contains a simplified breakdown of command IDs,
176 | command fields, and when values are needed for the different commands. Please also
177 | refer to the following [Examples](#examples) section for simple programs that help to
178 | inform how the Y2K interpreter works.
179 |
180 | ## Examples
181 |
182 | Each example below is taken directly from the [`examples`](examples) folder,
183 | but with added explanation for how/why they work.
184 |
185 | All examples can be exported to 0-byte solutions using the `-export` flag if
186 | desired.
187 |
188 |
189 |
190 | ### Set and Print Variable
191 | [`examples/set-and-print-var.y2k`](examples/set-and-print-var.y2k)
192 |
193 | Timestamp:
194 | - `812419999211000000 (1995-09-29 18:13:19.211000000)`
195 |
196 | This example simply sets an integer variable to the value `1999` and then
197 | prints that variable to the console.
198 |
199 | ```elixir
200 | 8124 # Create new variable 1 with type int (2) and size 4
201 | 1999 # Insert 4 digits (1999) into variable 1
202 |
203 | 9211 # Print variable 1
204 | ```
205 |
206 | Output: `1999`
207 |
208 |
209 |
210 | ### Modify and Print Variable
211 | [`examples/modify-and-print-var.y2k`](examples/modify-and-print-var.y2k)
212 |
213 | Timestamp:
214 | - `812419997120149211 (1995-09-29 18:13:17.120149211)`
215 |
216 | This example expands on the previous example by modifying the variable's
217 | value after creating it. In this case, we're taking the original variable
218 | value (`1999`) and subtracting `4` from it to get `1995`.
219 |
220 | ```elixir
221 | 8124 # Create (8) a variable (1) with type "int" (2) and size 4
222 | 1999 # Insert 4 digits (1999) into variable 1
223 |
224 | 71201 # On variable 1, call function "-=" (2) with a primitive (0) 1-digit argument
225 | 4 # Insert 1 digit (4) into function argument
226 |
227 | 9211 # Print variable 1
228 | ```
229 |
230 | Output: `1995`
231 |
232 |
233 |
234 | ### Hello World
235 | [`examples/hello-world.y2k`](examples/hello-world.y2k)
236 |
237 | Timestamp(s):
238 | - `502090112340512121 (1985-11-28 22:28:32.340512121)`
239 | - `850049151812046300 (1996-12-08 05:45:51.812046300)`
240 |
241 | In this example, we're printing the string "Hello World!". Since character
242 | codes are easier to encapsulate with 2-digit codes (and the string we're
243 | printing is a 2-digit number), we need to switch the interpreter to 2-digit
244 | parsing mode at the very beginning.
245 |
246 | ```elixir
247 | 5 0 2 # Switch interpreter to 2-digit parsing size
248 |
249 | 09 01 12 # Begin printing a string with a size of 12
250 |
251 | 34 05 12 12 15 00 # Write "Hello "
252 | 49 15 18 12 04 63 # Write "World!"
253 | ```
254 |
255 | Output: `Hello World!`
256 |
257 |
258 |
259 | ### Area of a Circle
260 | [`examples/area-of-circle.y2k`](examples/area-of-circle.y2k)
261 |
262 | Timestamp(s):
263 | - `813913141592679501 (1995-10-17 00:59:01.592679501)`
264 | - `827131199211000000 (1996-03-17 23:39:59.211000000)`
265 |
266 | In this example, we're introducing a couple of new concepts. One is the ability
267 | to include variables from the command line, and the other is modifying one
268 | variable using another variable's value.
269 |
270 | To include variable's from the command line, we simply pass the value after the
271 | input. For example, `y2k my-program.y2k 10` would include a variable with the
272 | value `10` that we can access from the beginning of the program. Since most Y2K
273 | programs create variables using sequential IDs (i.e. 0 -> 1 -> 2, etc),
274 | variables added from the command line are added to the back of the variable
275 | map, with descending IDs from there. So if you're running Y2K in the default
276 | 1-digit parser mode, command line arguments are added as variables with IDs
277 | starting at 9, then 8, and so on. As an example: `y2k my-program.y2k foo bar`
278 | would have variable 9 set to "foo" and variable 8 set to "bar".
279 |
280 | The other new concept is modifying a variable with the value from another
281 | variable. In previous examples, we've used primitive types for arguments, but
282 | in this case we need to multiply our "Pi" variable (1) by our squared radius.
283 | To do this, we set the third field to "1" to tell the interpreter that the
284 | value we're passing in is a variable ID, not a primitive type.
285 |
286 | ```elixir
287 | 8139 # Set variable 1 to type float (3) and size 9
288 |
289 | 131415926 # Insert 9 digits (131415926) into variable 1, using the first
290 | # digit (1) as the decimal placement (3.1415926)
291 |
292 | 79501 # Modify variable 9 (CLI arg) using the "**=" function (5),
293 | # with a non-variable (0) argument size of 1
294 | 2 # Use the number 2 as the function argument (var9 **= 2)
295 |
296 | 71311 # Modify variable 1 using the "*=" function (3), with a
297 | # variable argument (1) with a variable ID size of 1
298 | 9 # Use the variable ID 9 in the function argument (var1 *= var9)
299 |
300 | 9211 # Print variable 1
301 | ```
302 |
303 | Output (`y2k examples/area-of-circle.y2k 10`):
304 |
305 | ```
306 | 314.15926
307 | ```
308 |
309 | Output (`y2k examples/area-of-circle.y2k 25`):
310 |
311 | ```
312 | 1963.495375
313 | ```
314 |
315 |
316 |
317 | ### Fibonacci Sequence
318 | [`examples/fibonacci-n-terms.y2k`](examples/fibonacci-n-terms.y2k)
319 |
320 | Timestamp(s):
321 | - `812108221183210693 (1995-09-26 03:37:01.183210693)`
322 | - `811092117391117191 (1995-09-14 09:21:57.391117191)`
323 | - `812721113792011000 (1995-10-03 05:51:53.792011000)`
324 |
325 | For this modification to the Fibonacci Sequence program, we're now using an
326 | argument from the command line as the number of terms to print. In this new
327 | program, we'll take the command line argument and create a new loop that
328 | decrements that value until it reaches 0.
329 |
330 | We need 3 variables for this program, not including the variable added from the
331 | command line: a variable for the "current" value, a "placeholder" variable to
332 | track the "current" value before it gets updated, and a "next" variable to track
333 | the "next" value in the sequence. On each loop iteration, we 1) print "current", 2)
334 | set "placeholder" to "current", 3) set "current" to "next", 4) add "placeholder"
335 | to "next", and 5) decrement counter.
336 |
337 | ```elixir
338 | 8121 # Create variable 1 with type int (2) and size 1
339 | 0 # Insert 1 digit (0) into variable 1
340 | 8221 # Create variable 2 with type int (2) and size 1
341 | 1 # Insert 1 digit (1) into variable 2
342 | 8321 # Create variable 3 with type int (2) and size 1
343 | 0 # Insert 1 digit (0) into variable 3
344 |
345 | # Init while loop (while var 9 > 0)
346 | 69311 # Create conditional using variable 9, with comparison ">" (3),
347 | # as a loop (1), and with a right hand value size of 1
348 | 0 # Insert 1 digit (0) into conditional's right hand value
349 |
350 | # Begin while loop
351 | 9211 # Print var 9
352 | 739111 # var 3 = var 1
353 | 719112 # var 1 = var 2
354 | 721113 # var 2 += var 3
355 | 792011 # var 9 -= 1
356 | ```
357 |
358 | Output 1 (`y2k examples/fibonacci-n-terms.y2k 15`):
359 |
360 | ```
361 | 0
362 | 1
363 | 1
364 | 2
365 | 3
366 | 5
367 | 8
368 | 13
369 | 21
370 | 34
371 | 55
372 | 89
373 | 144
374 | 233
375 | 377
376 | ```
377 |
378 | Output 2 (`y2k examples/fibonacci-n-terms.y2k 20`):
379 |
380 | ```
381 | 0
382 | 1
383 | 1
384 | 2
385 | 3
386 | 5
387 | 8
388 | 13
389 | 21
390 | 34
391 | 55
392 | 89
393 | 144
394 | 233
395 | 377
396 | 610
397 | 987
398 | 1597
399 | 2584
400 | 4181
401 | ```
402 |
403 |
404 |
405 | ### Fizz Buzz
406 | [`examples/fizz-buzz.y2k`](examples/fizz-buzz.y2k)
407 |
408 | Timestamp(s):
409 | - `502080901043209262 (1985-11-28 19:55:01.043209262)`
410 | - `860808010428212626 (1997-04-11 19:20:10.428212626)`
411 | - `805000187919771118 (1995-07-05 21:09:47.919771118)`
412 | - `861213100711011614 (1997-04-16 11:51:40.711011614)`
413 | - `802159217420006140 (1995-06-03 00:00:17.420006140)`
414 | - `813921942000614015 (1995-10-17 03:25:42.000614015)`
415 | - `892184200092110000 (1998-04-09 22:56:40.092110000)`
416 |
417 | The Fizz Buzz program highlights a few features that haven't been covered yet,
418 | namely terminating and "continue"-ing conditionals. We also have to tell the
419 | interpreter to switch between 1- and 2-digit parsing in order to create our
420 | words "fizz" and "buzz" while maintaining the efficiency of 1-digit parsing.
421 |
422 | The value `2000`, when used within a non-looped conditional, tells the
423 | interpreter where the "body" of the statement needs to end. This is an
424 | arbitrarily chosen value (but fits with the name of the language) that is used
425 | multiple times in this program to tell the interpreter where an "if" statement
426 | ends. There's also the new command ID `4` (aka `CONTINUE`), which returns an
427 | empty string to the parent parser function instead of the remainder of the
428 | timestamp. Since this is being used inside a "while" loop, this returns the
429 | interpreter back to the beginning of the loop to reevaluate instead of
430 | continuing to the next part of the timestamp.
431 |
432 |
433 | ```elixir
434 | 502 # Change interpreter to 2-digit parsing mode
435 |
436 | # Set variables 9 and 8 to "fizz" and "buzz" respectively
437 | 08 09 01 04 # Create variable 9 with type string (1) and length 4
438 | 32 09 26 26 # Insert 4 chars ("Fizz") into variable 9
439 | 08 08 01 04 # Create variable 8 with type string (1) and length 4
440 | 28 21 26 26 # Insert 4 chars ("Buzz") into variable 8
441 |
442 | 05 00 01 # Change interpreter back to 1-digit parsing mode
443 |
444 | # Set variable 7 to "fizzbuzz"
445 | 8791 # Create variable 7 with type "copy" (9) and length 1 (variable ID length)
446 | 9 # Use 1 digit variable ID (9) to copy values from var 9 to var 7
447 | 77111 # On variable 7, call function "+=" (5) using a variable (1) with a 1 digit ID
448 | 8 # Use 1 digit variable ID (8) to append values from var 8 to var 7
449 |
450 | # Begin the loop from 0 to 100
451 | 61213100 # while variable 1 < 100 (implicit creation of var 1)
452 | 711011 # var 1 += 1
453 | 6140215 # if var 1 % 15 == 0
454 | 9217 # print var 7 ("fizzbuzz")
455 | 4 # continue
456 | 2000 # end-if
457 | 614013 # if var 1 % 3 == 0
458 | 9219 # print var 9 ("fizz")
459 | 4 # continue
460 | 2000 # end-if
461 | 614015 # if var 1 % 5 == 0
462 | 9218 # print var 8 ("buzz")
463 | 4 # continue
464 | 2000 # end-if
465 | 9211 # print var 1
466 | ```
467 |
468 | Output:
469 | ```
470 | 1
471 | 2
472 | fizz
473 | 4
474 | buzz
475 | fizz
476 | 7
477 | 8
478 | fizz
479 | buzz
480 | 11
481 | fizz
482 | 13
483 | 14
484 | fizzbuzz
485 | 16
486 | 17
487 | fizz
488 | 19
489 | buzz
490 | fizz
491 | 22
492 | 23
493 | fizz
494 | buzz
495 | 26
496 | fizz
497 | 28
498 | 29
499 | fizzbuzz
500 | ......
501 | ```
502 |
503 |
504 |
505 | ### Count Up Forever
506 | [`examples/count-up-forever.y2k`](examples/count-up-forever.y2k)
507 |
508 | Timestamp(s):
509 | - `611110721011921200 (1989-05-13 18:58:41.011921200)`
510 |
511 | *Originally from [this problem on
512 | codegolf.stackexchange.com](https://codegolf.stackexchange.com/questions/63834/count-up-forever/).*
513 |
514 | You may have noticed something "sneaky" in the Fizz Buzz example, which is the
515 | implicit creation of variables when they're referenced without being explicitly
516 | created beforehand. This is somewhat similar to the `for i in val` behavior
517 | seen in other languages, where `i` is created from the context it's used in.
518 | Currently, Y2K only supports initialization of integer variables with a value
519 | of `0`, but this could be expanded in future versions to support iterating over
520 | strings, arrays, and so on.
521 |
522 | This program -- a simple program to count up by 1 until killed -- highlights
523 | that feature. In this example, we create variable 1 through its reference in
524 | the while loop, and variable 2 the first time that we try to modify it.
525 |
526 | Creating variables this way isn't necessarily recommended, since it makes
527 | programs more difficult to read and, as previously mentioned, can only be used
528 | for creating variables with a value of 0. But it can be a useful way to
529 | condense a solution into an even smaller footprint. In this case, we can fit
530 | the solution to the problem in a single file timestamp (and in raw format is
531 | only 15 bytes after comments and newlines are removed).
532 |
533 | ```
534 | 611110 # while var 1 == 0
535 | 721011 # var 2 += 1
536 | 9212 # print var 2
537 | ```
538 |
539 | Output:
540 | ```
541 | 1
542 | 2
543 | 3
544 | 4
545 | 5
546 | ......
547 | ```
548 |
549 |
550 |
551 | ## FAQ
552 |
553 | - **Why the pre-2000 timestamp limitation? Why the name Y2K?**
554 |
555 | The language was originally designed to interpret timestamps of any length, but
556 | both macOS and Go store Unix nanoseconds as an int64. The max value of an int64
557 | has 19 digits (`9223372036854775807`) but it wouldn't be reliable to write
558 | programs using all 19 digits, since there can be programs that exceed this
559 | value fairly easily (a program to print the letter 'c' would start with
560 | `923...`, for example). As a result, all timestamps for Y2K programs have 18
561 | digits, which results in a maximum timestamp that falls around the year 2000¹.
562 |
563 | The interpreter was also originally designed to only ever read 2 digits at a time.
564 | These combined limitations reminded me of [the "Y2K
565 | Problem"](https://en.wikipedia.org/wiki/Year_2000_problem), hence the name.
566 |
567 | - **What does 0-byte actually mean? How can a program be 0 bytes?**
568 |
569 | Since the interpreter only reads file *timestamps* and not file *content*, each
570 | `.y2k` file can be completely empty (0 bytes) without affecting how each
571 | program is interpreted. And since every file has to have a timestamp associated
572 | with it anyway, there aren't any extra bytes needed to achieve this
573 | functionality. Technically though, there's no such thing as a 0 byte file --
574 | the metadata for that file does have to be stored somewhere. But for code
575 | golfing purposes, I believe it would be counted as 0 bytes.
576 |
577 | - **Why are there two ways to copy a variable's value to a new variable?**
578 |
579 | The method through the `SET` command (`8`) inserts a new reference to a
580 | variable using the specified ID, whereas the method through the `MODIFY`
581 | command (`7`) updates the existing reference in the variable table. The former
582 | can be useful for instantiating a new variable from an existing one, but can
583 | cause problems if you're within the scope of a condition that has referenced
584 | that variable.
585 |
586 | For example:
587 |
588 | ```elixir
589 | 81210 # int var 1 = 0
590 | 82210 # int var 2 = 0
591 |
592 | # BAD
593 | # Loops infinitely, since the reference to Var 1 that
594 | # was used to create the loop is overwritten, and the
595 | # value of the original reference is never updated
596 | 61213100 # While Var 1 < 100
597 | 721101 # Var 2 += 1
598 | 81912 # Overwrite Var 1 with Var 2 values
599 |
600 | # GOOD
601 | # Loops as expected, Var 1's value is updated on each
602 | # iteration with Var 2's value
603 | 61213100 # While Var 1 < 100
604 | 721101 # Var 2 += 1
605 | 719112 # Copy Var 2 value to Var 1
606 | ```
607 |
608 | - **How would I show proof of my solution in a code golf submission?**
609 |
610 | I'm not sure the best way to do this yet. Assuming you wrote your solution
611 | as a "raw" Y2K file, you can run `y2k -export my-program.y2k`, and then
612 | run the following command:
613 |
614 | ```shell
615 | $ ls ./y2k-out/*.y2k -lo --time-style="+%s%9N"
616 | -rw-r--r-- 1 benbusby 0 502090134051212150 0.y2k
617 | -rw-r--r-- 1 benbusby 0 104915181204630000 1.y2k
618 | ```
619 |
620 | You could also include your raw Y2K file contents along with the 0-byte
621 | proof, to be extra thorough.
622 |
623 | - **Why doesn't Y2K have X feature?**
624 |
625 | The language is still in development. Feel free to open an issue, or refer to
626 | the [Contributing](#contributing) section if you'd like to help out!
627 |
628 | _____
629 |
630 | ¹ Technically Sept. 2001, but close enough...
631 |
632 | ## Contributing
633 |
634 | I would appreciate any input/contributions from anyone. Y2K still needs a lot
635 | of work, so feel free to submit a PR for a new feature, browse the issues tab
636 | to see if there's anything that you're interested in working on, or add a new
637 | example program.
638 |
639 | The main thing that would help is trying to solve current or past code-golfing
640 | problems from https://codegolf.stackexchange.com. If there's a limitation in
641 | Y2K (there are definitely a ton) that prevents you from solving the problem,
642 | open an issue or PR so that it can be addressed!
643 |
--------------------------------------------------------------------------------
/cross-compile.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | mkdir -p out/
4 | rm -rf out/*
5 |
6 | platforms=(
7 | "windows/386"
8 | "windows/amd64"
9 | "windows/arm64"
10 | "darwin/amd64"
11 | "darwin/arm64"
12 | "linux/arm"
13 | "linux/amd64"
14 | "linux/arm64"
15 | "linux/386")
16 |
17 | for platform in "${platforms[@]}"
18 | do
19 | echo "Compiling for $platform..."
20 | platform_split=(${platform//\// })
21 | GOOS=${platform_split[0]}
22 | GOARCH=${platform_split[1]}
23 |
24 | output_name="y2k-$GOOS-$GOARCH"
25 | if [ $GOOS = "darwin" ]; then
26 | output_name="y2k-macos-$GOARCH"
27 | elif [ $GOARCH = "arm" ]; then
28 | output_name="y2k-$GOOS-arm32"
29 | elif [ $GOOS = "windows" ]; then
30 | output_name+='.exe'
31 | fi
32 |
33 | GOOS=$GOOS GOARCH=$GOARCH go build -ldflags="-s -w" -o out/$output_name $package
34 | if [ $? -ne 0 ]; then
35 | echo 'An error has occurred! Aborting the script execution...'
36 | exit 1
37 | fi
38 | done
39 |
--------------------------------------------------------------------------------
/examples/area-of-circle.y2k:
--------------------------------------------------------------------------------
1 | # area-of-circle.y2k
2 | # This program calculates the area of a circle using a command line
3 | # input as the radius.
4 |
5 | 8139 # Set variable 1 to type float (3) and size 9
6 | 131415926 # Insert 9 digits (131415926) into variable 1, using the first
7 | # digit (1) as the decimal placement (3.1415926)
8 |
9 | 79501 # Modify variable 9 (CLI arg) using the "**=" function (5),
10 | # with a non-variable (0) argument size of 1
11 | 2 # Use the number 2 as the function argument (var9 **= 2)
12 |
13 | 71311 # Modify variable 1 using the "*=" function (3), with a
14 | # variable argument (1) with a variable ID size of 1
15 | 9 # Use the variable ID 9 in the function argument (var1 *= var9)
16 |
17 | 9211 # Print variable 1
18 |
--------------------------------------------------------------------------------
/examples/count-up-forever.y2k:
--------------------------------------------------------------------------------
1 | # count-up-forever.y2k :: SKIP_TEST
2 | # This program counts up by 1 in a loop indefinitely. It serves as an
3 | # example of how you can instantiate variables through references to
4 | # nonexistent variables. In this case, neither var 1 or var 2 exist before
5 | # they're referenced, but are created (with a value of 0) in their respective
6 | # commands.
7 |
8 | 611110 # while var 1 == 0
9 | 721011 # var 2 += 1
10 | 9212 # print var 2
11 |
--------------------------------------------------------------------------------
/examples/fibonacci-n-terms.y2k:
--------------------------------------------------------------------------------
1 | # fibonacci-n-terms.y2k
2 | # This program reads an integer command line argument and prints the
3 | # equivalent number of terms of the Fibonacci Sequence.
4 | # Command line arguments are added to the end of the variable map,
5 | # depending on the digit size of the parser (i.e. digit size 1 means
6 | # CLI args are inserted at 9 and descending, digit size 2 means 99,
7 | # etc).
8 |
9 | 8121 # Create variable 1 with type int (2) and size 1
10 | 0 # Insert 1 digit (0) into variable 1
11 | 8221 # Create variable 2 with type int (2) and size 1
12 | 1 # Insert 1 digit (1) into variable 2
13 | 8321 # Create variable 3 with type int (2) and size 1
14 | 0 # Insert 1 digit (0) into variable 3
15 |
16 | # Init while loop (while var 9 > 0)
17 | 69311 # Create conditional using variable 9, with comparison ">" (3),
18 | # as a loop (1), and with a right hand value size of 1
19 | 0 # Insert 1 digit (0) into conditional's right hand value
20 |
21 | # Begin while loop
22 | 9211 # Print var 9
23 | 739111 # var 3 = var 1
24 | 719112 # var 1 = var 2
25 | 721113 # var 2 += var 3
26 | 792011 # var 9 -= 1
27 |
--------------------------------------------------------------------------------
/examples/fizz-buzz.y2k:
--------------------------------------------------------------------------------
1 | # fizz-buzz.y2k
2 | # This program loops through all numbers from 1-100. If the number is
3 | # divisible by 3, it prints "fizz". If it's divisible by 5, it prints
4 | # "buzz". If it's divisible by both 3 and 5, it prints "fizzbuzz". If
5 | # none of those conditions are met, it just prints the number.
6 |
7 | 502 # Change interpreter to 2-digit parsing mode
8 |
9 | # Set variables 9 and 8 to "fizz" and "buzz" respectively
10 | 08 09 01 04 # Create variable 9 with type string (1) and length 4
11 | 32 09 26 26 # Insert 4 chars ("Fizz") into variable 9
12 | 08 08 01 04 # Create variable 8 with type string (1) and length 4
13 | 28 21 26 26 # Insert 4 chars ("Buzz") into variable 8
14 |
15 | 05 00 01 # Change interpreter back to 1-digit parsing mode
16 |
17 | # Set variable 7 to "fizzbuzz"
18 | 8791 # Create variable 7 with type "copy" (9) and length 1 (variable ID length)
19 | 9 # Use 1 digit variable ID (9) to copy values from var 9 to var 7
20 | 77111 # On variable 7, call function "+=" (5) using a variable (1) with a 1 digit ID
21 | 8 # Use 1 digit variable ID (8) to append values from var 8 to var 7
22 |
23 | # Begin the loop from 0 to 100
24 | 61213100 # while variable 1 < 100 (implicit creation of var 1)
25 | 711011 # var 1 += 1
26 | 6140215 # if var 1 % 15 == 0
27 | 9217 # print var 7 ("fizzbuzz")
28 | 4 # continue
29 | 2000 # end-if
30 | 614013 # if var 1 % 3 == 0
31 | 9219 # print var 9 ("fizz")
32 | 4 # continue
33 | 2000 # end-if
34 | 614015 # if var 1 % 5 == 0
35 | 9218 # print var 8 ("buzz")
36 | 4 # continue
37 | 2000 # end-if
38 | 9211 # print var 1
39 |
--------------------------------------------------------------------------------
/examples/hello-world.y2k:
--------------------------------------------------------------------------------
1 | # hello-world.y2k
2 | # This program prints the string "Hello World!"
3 |
4 | 5 0 2 # Switch interpreter to 2-digit parsing size
5 |
6 | 09 01 12 # Begin printing string with a size of 12
7 |
8 | 34 05 12 12 15 00 # Write "Hello "
9 | 49 15 18 12 04 63 # Write "World!"
10 |
--------------------------------------------------------------------------------
/examples/modify-and-print-var.y2k:
--------------------------------------------------------------------------------
1 | # modify-and-print-var.y2k
2 | # This program creates a variable 1 with the value 100, subtracts 500
3 | # from the variable, and then prints the variable.
4 |
5 | 8124 # Create new variable 1 with type int (2) and size 3
6 | 1999 # Insert 3 digits (100) into variable 1
7 |
8 | 71201 # On variable 1, call function "-=" (2) with a primitive (0) 1-digit argument
9 | 4 # Insert 1 digit (4) into function argument
10 |
11 | 9211 # Print variable 1
12 |
--------------------------------------------------------------------------------
/examples/set-and-print-var.y2k:
--------------------------------------------------------------------------------
1 | # set-and-print-var.y2k
2 | # This program sets variable 1 to the integer value 1999, and then prints
3 | # the variable.
4 |
5 | 8124 # Create new variable 1 with type int (2) and size 4
6 | 1999 # Insert 2 digits (99) into variable 1
7 |
8 | 9211 # Print variable 1
9 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/benbusby/y2k
2 |
3 | go 1.19
4 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "github.com/benbusby/y2k/src/interpreter"
7 | "github.com/benbusby/y2k/src/utils"
8 | )
9 |
10 | func main() {
11 | var timestamp string
12 | digits := flag.Int(
13 | "d",
14 | 1,
15 | "Set # of digits to parse at a time")
16 | debug := flag.Bool(
17 | "debug",
18 | false,
19 | "Enable to view interpreter steps in console")
20 | export := flag.Bool(
21 | "export",
22 | false,
23 | "Export a Y2K raw file to a set of timestamp-only files")
24 | outdir := flag.String(
25 | "outdir",
26 | "./y2k-out",
27 | "Set the output directory for timestamp-only files when exporting a raw Y2K file.\n"+
28 | "This directory will be created if it does not exist.")
29 | flag.Parse()
30 |
31 | y2k := &interpreter.Y2K{Digits: *digits, Debug: *debug}
32 |
33 | for _, arg := range flag.Args() {
34 | // Assume first argument is the directory or file to use for parsing
35 | if len(timestamp) == 0 {
36 | if *export {
37 | // If we're exporting, assume we're only reading raw Y2K file
38 | // contents, and export to a set of empty files.
39 | timestamp = utils.ReadY2KRawFile(arg)
40 |
41 | utils.ExportRawToTimestampFiles(timestamp, *outdir)
42 | return
43 | } else {
44 | timestamp = utils.GetTimestamps(arg, *digits)
45 | }
46 | continue
47 | }
48 |
49 | y2k.FromCLIArg(arg)
50 | }
51 |
52 | if len(timestamp) == 0 {
53 | fmt.Println("Missing input dir!\n\nUsage: y2k [args]")
54 | flag.PrintDefaults()
55 | return
56 | }
57 |
58 | y2k.Parse(timestamp)
59 | }
60 |
--------------------------------------------------------------------------------
/src/interpreter/condition.go:
--------------------------------------------------------------------------------
1 | package interpreter
2 |
3 | import (
4 | "github.com/benbusby/y2k/src/utils"
5 | "math"
6 | "reflect"
7 | "strings"
8 | )
9 |
10 | // ComparisonMap holds an int->function mapping to compare a variable against
11 | // an arbitrary value.
12 | var ComparisonMap = map[uint8]func(*Y2KVar, []string) bool{
13 | 1: EqualTo,
14 | 2: LessThan,
15 | 3: GreaterThan,
16 | 4: IsDivisible,
17 | }
18 |
19 | type Y2KCond struct {
20 | VarID uint8
21 | CompFn uint8
22 | Loop bool
23 | CompValSize uint8
24 | value string
25 | }
26 |
27 | // RunCond evaluates a timestamp as either a loop or a standalone "if" statement.
28 | // All conditions compare a variable's value against a slice of strings, with the
29 | // latter getting converted to the variable's data type in the comparison function.
30 | // For example, if comparing an integer variable against ["8", "9"], the integer
31 | // would need to have the number 89 stored as its numeric value. If comparing a
32 | // string variable, it would need to have "hi" stored as its string value.
33 | func (y2kComp Y2KCond) RunCond(
34 | y2k Y2K,
35 | timestamp string,
36 | target *Y2KVar,
37 | splitComp []string,
38 | ) bool {
39 | var result string
40 |
41 | if y2kComp.Loop {
42 | for ComparisonMap[y2kComp.CompFn](target, splitComp) {
43 | result = y2k.Parse(timestamp)
44 | }
45 | } else {
46 | if ComparisonMap[y2kComp.CompFn](target, splitComp) {
47 | result = y2k.Parse(timestamp)
48 | }
49 | }
50 |
51 | if result == utils.ContinueCmd {
52 | // Break value of timestamp if commanded
53 | return true
54 | }
55 |
56 | return false
57 | }
58 |
59 | // EqualTo checks string or numeric equality
60 | func EqualTo(y2kVar *Y2KVar, values []string) bool {
61 | if y2kVar.Type == Y2KString {
62 | return y2kVar.strVal == utils.StrArrToPrintable(values)
63 | }
64 |
65 | return y2kVar.numVal == utils.StrArrToFloat(values)
66 | }
67 |
68 | // LessThan checks if a string's length is less than the specified value,
69 | // or if a number is less than a different numeric value.
70 | func LessThan(y2kVar *Y2KVar, values []string) bool {
71 | if y2kVar.Type == Y2KString {
72 | return len(y2kVar.strVal) < utils.StrArrToInt(values)
73 | }
74 |
75 | return y2kVar.numVal < utils.StrArrToFloat(values)
76 | }
77 |
78 | // GreaterThan checks if a string's length is greater than the specified value,
79 | // or if a number is greater than a different numeric value.
80 | func GreaterThan(y2kVar *Y2KVar, values []string) bool {
81 | if y2kVar.Type == Y2KString {
82 | return len(y2kVar.strVal) > utils.StrArrToInt(values)
83 | }
84 |
85 | return y2kVar.numVal > utils.StrArrToFloat(values)
86 | }
87 |
88 | // IsDivisible checks if a numeric variable is evenly divisible by a
89 | // specific number. Currently, there isn't an equivalent for string
90 | // variables, so this will just return true in that case.
91 | func IsDivisible(y2kVar *Y2KVar, values []string) bool {
92 | if y2kVar.Type == Y2KString {
93 | return true
94 | }
95 |
96 | return math.Mod(y2kVar.numVal, utils.StrArrToFloat(values)) == 0
97 | }
98 |
99 | // ParseCondition compares a variable against a raw value and parses a segment of
100 | // the timestamp until the comparison is false. The segment of the timestamp
101 | // used for the loop is determined by a function terminator ("1999") or the end
102 | // of the timestamp if the terminator is not found.
103 | func (y2k Y2K) ParseCondition(timestamp string, val reflect.Value) string {
104 | y2kCond := val.Interface().(Y2KCond)
105 |
106 | input := timestamp[:y2k.Digits]
107 | y2k.DebugMsg("ParseCondition: [%s]%s",
108 | input,
109 | timestamp[y2k.Digits:])
110 |
111 | y2kCond.value += input
112 |
113 | if len(y2kCond.value) >= int(y2kCond.CompValSize) {
114 | targetVar := GetVar(y2kCond.VarID)
115 |
116 | // CompFn functions need the raw comparison value passed to
117 | // them, because they treat values differently depending on the
118 | // target variable data type. It's easier to parse the comparison
119 | // value in as a string and then convert it back to a slice of
120 | // N-size strings than it is to create the slice during parsing, due
121 | // to differences in y2k.Digits values. For example -- parsing a 3
122 | // digit number "100XX..." with a 2-digit window would create a
123 | // slice of ["10", "0X"], where X is an unrelated digit for a
124 | // subsequent command. Parsing it as a string and then splitting it,
125 | // however, creates ["10", "0"].
126 | splitComp := utils.SplitStrByN(
127 | y2kCond.value[:y2kCond.CompValSize],
128 | y2k.Digits)
129 |
130 | // Extract the index of the cond terminator and the subset of the
131 | // timestamp that should be returned to the main interpreter loop.
132 | condTerm := utils.GetCondTerm(y2kCond.Loop)
133 | timestampFnTerm := strings.Index(timestamp, condTerm)
134 | nextIterTimestamp := timestamp[timestampFnTerm+len(condTerm)-1:]
135 |
136 | // If there isn't a function terminator, assume that the condition
137 | // terminates at the end of the timestamp.
138 | if timestampFnTerm < 0 {
139 | timestampFnTerm = len(timestamp)
140 | nextIterTimestamp = ""
141 | }
142 |
143 | // Determine the segment of the timestamp that will be parsed on
144 | // each iteration of the while loop.
145 | whileTimestamp := timestamp[y2k.Digits:timestampFnTerm]
146 | y2k.DebugMsg(utils.DebugDivider)
147 |
148 | stop := y2kCond.RunCond(y2k, whileTimestamp, targetVar, splitComp)
149 |
150 | // Conditions can optionally break value of the timestamp using the
151 | // CONTINUE command (see interpreter.go). In this instance, the next
152 | // timestamp passed back to the parser should be empty.
153 | if stop {
154 | return ""
155 | }
156 |
157 | return nextIterTimestamp
158 | }
159 |
160 | return y2k.ParseCondition(timestamp[y2k.Digits:], reflect.ValueOf(y2kCond))
161 | }
162 |
--------------------------------------------------------------------------------
/src/interpreter/interpreter.go:
--------------------------------------------------------------------------------
1 | package interpreter
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "github.com/benbusby/y2k/src/utils"
7 | "os"
8 | "reflect"
9 | )
10 |
11 | type Y2K struct {
12 | Debug bool
13 | Digits int
14 | }
15 |
16 | type Instruction struct {
17 | val reflect.Value
18 | fn func(Y2K, string, reflect.Value) string
19 | }
20 |
21 | type Y2KCommand uint8
22 |
23 | const (
24 | PRINT Y2KCommand = 9
25 | CREATE Y2KCommand = 8
26 | MODIFY Y2KCommand = 7
27 | CONDITION Y2KCommand = 6
28 | META Y2KCommand = 5
29 | CONTINUE Y2KCommand = 4
30 | )
31 |
32 | var instMap map[Y2KCommand]Instruction
33 | var stdout = bufio.NewWriter(os.Stdout)
34 |
35 | // CreateStruct uses reflection to form a struct from N-sized chunks
36 | // from the timestamp. The struct that is constructed is mapped to
37 | // a Y2KCommand and holds all values that are relevant to performing
38 | // the specified command (i.e. Y2KVar establishes variable ID,
39 | // size, and type).
40 | func (y2k Y2K) CreateStruct(
41 | timestamp string,
42 | v reflect.Value,
43 | ) (reflect.Value, string) {
44 | modFields := 0
45 |
46 | for i := 0; i < v.NumField(); i++ {
47 | // Ignore private struct fields
48 | if !v.Field(i).CanSet() {
49 | continue
50 | }
51 |
52 | idx := y2k.Digits * modFields
53 | val := utils.StrToInt(timestamp[idx : idx+y2k.Digits])
54 |
55 | // Fetching the string names of the struct name and fields is
56 | // pretty expensive, so we're checking debug here before evaluating
57 | if y2k.Debug {
58 | structName := v.Type().Name()
59 | fieldName := v.Type().Field(i).Name
60 |
61 | y2k.DebugMsg("%s.%s: [%s]%s",
62 | structName,
63 | fieldName,
64 | timestamp[idx:idx+y2k.Digits],
65 | timestamp[idx+y2k.Digits:],
66 | )
67 | }
68 |
69 | switch v.Field(i).Type().Kind() {
70 | case reflect.Int:
71 | fallthrough
72 | case reflect.Int8:
73 | v.Field(i).SetInt(int64(val))
74 | break
75 | case reflect.Uint:
76 | fallthrough
77 | case reflect.Uint8:
78 | v.Field(i).SetUint(uint64(val))
79 | break
80 | case reflect.Bool:
81 | v.Field(i).SetBool(val != 0)
82 | break
83 | default:
84 | panic(fmt.Sprintf(
85 | "Unhandled type reflection: %s (in %s)",
86 | v.Field(i).Type().Kind(),
87 | v.String()))
88 | }
89 |
90 | modFields += 1
91 | }
92 |
93 | newStart := y2k.Digits * modFields
94 | return v, timestamp[newStart:]
95 | }
96 |
97 | // DebugMsg is used for printing useful info about what operations the
98 | // interpreter is performing, and inspecting the values from the timestamps
99 | // that are being interpreted.
100 | func (y2k Y2K) DebugMsg(template string, input ...string) {
101 | if y2k.Debug {
102 | args := make([]interface{}, len(input))
103 | for i, s := range input {
104 | args[i] = s
105 | }
106 | y2k.OutputMsg(fmt.Sprintf(template, args...))
107 | }
108 | }
109 |
110 | // OutputMsg uses a buffered stdout writer to output messages from Y2K. It's
111 | // slightly more performant than fmt.Println.
112 | func (y2k Y2K) OutputMsg(msg string) {
113 | defer func(stdout *bufio.Writer) {
114 | err := stdout.Flush()
115 | utils.Check(err)
116 | }(stdout)
117 |
118 | _, err := stdout.WriteString(msg)
119 | _, err = stdout.WriteString("\n")
120 | utils.Check(err)
121 | }
122 |
123 | // Parse manages interpreter state and hands off timestamp parsing to the
124 | // appropriate function when changes to interpreter state are made.
125 | // For example, creation of a variable jumps from STANDBY to CREATE states,
126 | // and moves timestamp parsing to ParseVariable until that function passes
127 | // parsing back to Parse.
128 | func (y2k Y2K) Parse(timestamp string) string {
129 | // Extract a portion of the timestamp, with size determined by the
130 | // Y2K.Digits field.
131 | y2k.DebugMsg("Parse: [%s]%s",
132 | timestamp[:y2k.Digits],
133 | timestamp[y2k.Digits:],
134 | )
135 | command := Y2KCommand(utils.StrToInt(timestamp[:y2k.Digits]))
136 |
137 | if command == CONTINUE {
138 | // Return early if a "continue" command is received
139 | return utils.ContinueCmd
140 | } else if instruction, ok := instMap[command]; ok {
141 | var y2kStruct reflect.Value
142 | y2kStruct, timestamp = y2k.CreateStruct(
143 | timestamp[y2k.Digits:],
144 | instruction.val)
145 | timestamp = instruction.fn(y2k, timestamp, y2kStruct)
146 | }
147 |
148 | if y2k.Digits > len(timestamp)-y2k.Digits {
149 | // Finished parsing
150 | return ""
151 | }
152 |
153 | return y2k.Parse(timestamp[y2k.Digits:])
154 | }
155 |
156 | func (y2k Y2K) ParseMeta(timestamp string, val reflect.Value) string {
157 | newY2K := val.Interface().(Y2K)
158 | return newY2K.Parse(timestamp)
159 | }
160 |
161 | func init() {
162 | instMap = map[Y2KCommand]Instruction{
163 | PRINT: {reflect.ValueOf(&Y2KPrint{}).Elem(), Y2K.ParsePrint},
164 | CREATE: {reflect.ValueOf(&Y2KVar{}).Elem(), Y2K.ParseVariable},
165 | MODIFY: {reflect.ValueOf(&Y2KMod{}).Elem(), Y2K.ParseModify},
166 | CONDITION: {reflect.ValueOf(&Y2KCond{}).Elem(), Y2K.ParseCondition},
167 | META: {reflect.ValueOf(&Y2K{}).Elem(), Y2K.ParseMeta},
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/interpreter/modifier.go:
--------------------------------------------------------------------------------
1 | package interpreter
2 |
3 | import (
4 | "github.com/benbusby/y2k/src/utils"
5 | "math"
6 | "reflect"
7 | "strings"
8 | )
9 |
10 | type Y2KMod struct {
11 | VarID uint8
12 | ModFn uint8
13 | ArgIsVar bool
14 | ModSize uint8
15 | value string
16 | }
17 |
18 | // modMap holds an int->function mapping to match timestamp input
19 | // to the appropriate function to perform on the specified variable.
20 | var modMap = map[uint8]func(*Y2KVar, string, float64){
21 | 1: AddToVar,
22 | 2: SubtractFromVar,
23 | 3: MultiplyVar,
24 | 4: DivideVar,
25 | 5: PowVar,
26 | 9: SetVar,
27 | }
28 |
29 | // AddToVar directly modifies a variable by adding a second value to either its
30 | // numVal or strVal property (depending on variable data type).
31 | func AddToVar(y2kVar *Y2KVar, strVal string, numVal float64) {
32 | if y2kVar.Type == Y2KString {
33 | y2kVar.strVal += strVal
34 | return
35 | }
36 |
37 | y2kVar.numVal += numVal
38 | }
39 |
40 | // SubtractFromVar modifies a variable by subtracting from the variable's value.
41 | // For strings, this results in a substring from 0:length-N. For all other
42 | // variable types, this is regular subtraction.
43 | func SubtractFromVar(y2kVar *Y2KVar, _ string, numVal float64) {
44 | if y2kVar.Type == Y2KString {
45 | y2kVar.strVal = y2kVar.strVal[0 : len(y2kVar.strVal)-int(numVal)]
46 | return
47 | }
48 |
49 | y2kVar.numVal -= numVal
50 | }
51 |
52 | // MultiplyVar directly modifies a variable by multiplying the value by a
53 | // number. For strings, this results in a string that is repeated N number of
54 | // times. For all other variable types, this is regular multiplication. Note
55 | // that in this case, val is always treated as a number, even for string
56 | // variables.
57 | func MultiplyVar(y2kVar *Y2KVar, _ string, numVal float64) {
58 | if y2kVar.Type == Y2KString {
59 | y2kVar.strVal = strings.Repeat(y2kVar.strVal, int(numVal))
60 | return
61 | }
62 |
63 | y2kVar.numVal *= numVal
64 | }
65 |
66 | // DivideVar modifies a variable by dividing the value by a number (if the
67 | // variable is numeric) or a string (if the variable is a string). For strings,
68 | // this results in a string with all instances of the specified string removed.
69 | // For all other variable types, this is regular division.
70 | // value Example: "hello world!" / "o" -> "hell wrld!"
71 | func DivideVar(y2kVar *Y2KVar, strVal string, numVal float64) {
72 | if y2kVar.Type == Y2KString {
73 | y2kVar.strVal = strings.ReplaceAll(y2kVar.strVal, strVal, "")
74 | return
75 | }
76 |
77 | y2kVar.numVal /= numVal
78 | }
79 |
80 | // PowVar returns the result of exponentiation with a variable's numeric
81 | // value as a base, and numVal input as the exponent.
82 | // This only applies to numeric variables -- string variables are ignored.
83 | func PowVar(y2kVar *Y2KVar, _ string, numVal float64) {
84 | if y2kVar.Type == Y2KString {
85 | return
86 | }
87 |
88 | y2kVar.numVal = math.Pow(y2kVar.numVal, numVal)
89 | }
90 |
91 | // SetVar overwrites a variable's value with the given input. Note that you
92 | // cannot overwrite a string variable with a numeric value. You would want
93 | // to create a new variable (command 8) with the new data type in that case.
94 | func SetVar(y2kVar *Y2KVar, strVal string, numVal float64) {
95 | if y2kVar.Type == Y2KString {
96 | y2kVar.strVal = strVal
97 | return
98 | }
99 |
100 | y2kVar.numVal = numVal
101 | }
102 |
103 | // ParseModify recursively builds a set of values to modify an existing
104 | // variable. The order of values are:
105 | //
106 | // -> -> ->
107 | //
108 | // Once the mod size has been reached, we can pass the mod value to the desired
109 | // function and return the timestamp back to the original caller.
110 | func (y2k Y2K) ParseModify(timestamp string, val reflect.Value) string {
111 | varMod := val.Interface().(Y2KMod)
112 |
113 | input := timestamp[:y2k.Digits]
114 | y2k.DebugMsg("ParseModify: [%s]%s",
115 | input,
116 | timestamp[y2k.Digits:],
117 | )
118 |
119 | varMod.value += input
120 |
121 | if len(varMod.value) >= int(varMod.ModSize) {
122 | // Although we have the desired size of the modification, we don't
123 | // know how the modification value needs to be interpreted. By
124 | // converting the mod value to a slice of strings, we can pass off
125 | // final interpretation of the value to the actual function that is
126 | // performing the modification. For example, adding to a string
127 | // should interpret inputs as a string ("h" + 9 == "hi"), but
128 | // multiplying a string should interpret the input as a number.
129 | // ("h" * 9 == "hhhhhhhhh").
130 | targetVar := GetVar(varMod.VarID)
131 | varMod.value = varMod.value[:varMod.ModSize]
132 |
133 | // Retrieve the possible str and num values of the provided values
134 | splitValue := utils.SplitStrByN(varMod.value, y2k.Digits)
135 | strVal := utils.StrArrToPrintable(splitValue)
136 | numVal := utils.StrArrToFloat(splitValue)
137 |
138 | // If the user specified that the argument is a variable, use the
139 | // provided input as a variable ID lookup and overwrite the values
140 | // determined earlier
141 | if varMod.ArgIsVar {
142 | argVar := GetVar(uint8(utils.StrArrToInt(splitValue)))
143 | strVal, numVal = argVar.GetValues()
144 | }
145 |
146 | modMap[varMod.ModFn](targetVar, strVal, numVal)
147 |
148 | return timestamp
149 | }
150 |
151 | return y2k.ParseModify(timestamp[y2k.Digits:], reflect.ValueOf(varMod))
152 | }
153 |
--------------------------------------------------------------------------------
/src/interpreter/print.go:
--------------------------------------------------------------------------------
1 | package interpreter
2 |
3 | import (
4 | "github.com/benbusby/y2k/src/utils"
5 | "reflect"
6 | )
7 |
8 | // Y2KPrintType is an enum to indicate to the interpreter what should be printed.
9 | type Y2KPrintType uint8
10 |
11 | const (
12 | Y2KPrintString Y2KPrintType = 1
13 | Y2KPrintVar Y2KPrintType = 2
14 | )
15 |
16 | type Y2KPrint struct {
17 | Type Y2KPrintType
18 | Size int
19 | value string
20 | }
21 |
22 | func (y2k Y2K) ParsePrint(timestamp string, val reflect.Value) string {
23 | y2kPrint := val.Interface().(Y2KPrint)
24 |
25 | input := timestamp[:y2k.Digits]
26 | y2k.DebugMsg("ParsePrint: [%s]%s",
27 | input,
28 | timestamp[y2k.Digits:],
29 | )
30 |
31 | y2kPrint.value += input
32 |
33 | if len(y2kPrint.value) >= y2kPrint.Size*y2k.Digits {
34 | // If we're printing a variable, the value will be an integer
35 | // variable ID to print. Otherwise, we need to split the string
36 | // into N-sized chunks (dependent on interpreter parsing window
37 | // size) and print each character that matches each digit.
38 | switch y2kPrint.Type {
39 | case Y2KPrintString:
40 | splitValues := utils.SplitStrByN(y2kPrint.value, y2k.Digits)
41 | strValue := utils.StrArrToPrintable(splitValues)
42 | y2k.OutputMsg(strValue)
43 | break
44 | case Y2KPrintVar:
45 | printVar := GetVar(uint8(utils.StrToInt(y2kPrint.value)))
46 | y2k.OutputMsg(printVar.GetValue())
47 | break
48 | }
49 |
50 | return timestamp
51 | }
52 |
53 | return y2k.ParsePrint(timestamp[y2k.Digits:], reflect.ValueOf(y2kPrint))
54 | }
55 |
--------------------------------------------------------------------------------
/src/interpreter/variable.go:
--------------------------------------------------------------------------------
1 | package interpreter
2 |
3 | import (
4 | "github.com/benbusby/y2k/src/utils"
5 | "reflect"
6 | "strconv"
7 | "strings"
8 | "unicode"
9 | )
10 |
11 | var VarMap = map[uint8]*Y2KVar{}
12 |
13 | // Y2KVarType is an enum to indicate how the interpreter should treat a Y2KVar.
14 | type Y2KVarType uint8
15 |
16 | const (
17 | Y2KString Y2KVarType = 1
18 | Y2KInt Y2KVarType = 2
19 | Y2KFloat Y2KVarType = 3
20 | Y2KVarCopy Y2KVarType = 9
21 | )
22 |
23 | // Y2KVar is a struct for all variables created by Y2K programs. These contain
24 | // both numeric and string values as well as a data type. When creating numeric
25 | // variables, the strVal property is used to construct a numeric value while
26 | // parsing, until the variable's Size is reached.
27 | type Y2KVar struct {
28 | ID uint8
29 | Type Y2KVarType
30 | Size uint8
31 | strVal string
32 | numVal float64
33 | }
34 |
35 | // GetValue returns the appropriate value for a particular variable. If it's a
36 | // numeric variable, it returns the numeric value, otherwise it returns the
37 | // string value.
38 | func (y2kVar *Y2KVar) GetValue() string {
39 | if y2kVar.Type == Y2KString {
40 | return y2kVar.strVal
41 | }
42 |
43 | return utils.FloatToString(y2kVar.numVal)
44 | }
45 |
46 | // GetValues returns both strVal and numVal of a variable.
47 | func (y2kVar *Y2KVar) GetValues() (string, float64) {
48 | return y2kVar.strVal, y2kVar.numVal
49 | }
50 |
51 | // GetVar retrieves a variable from the existing ID->var map,
52 | // or returns an empty version of the variable struct if the
53 | // request var id has not been set.
54 | func GetVar(id uint8) *Y2KVar {
55 | if variable, ok := VarMap[id]; ok {
56 | return variable
57 | }
58 |
59 | // If the variable has not been set yet, insert it now.
60 | VarMap[id] = &Y2KVar{}
61 | return VarMap[id]
62 | }
63 |
64 | // FromCLIArg takes a command line argument and turns it into a variable for the
65 | // programs to reference as needed. Variables added from the command line are
66 | // inserted into the map backwards from the map's max index (9 for 1-digit
67 | // parsing, 99 for 2-digit parsing, etc).
68 | func (y2k Y2K) FromCLIArg(input string) {
69 | // Determine if the argument is a string or numeric.
70 | // Assume the variable is numeric, unless a non-numeric other than '.' is
71 | // found.
72 | argType := Y2KInt
73 | for _, c := range input {
74 | if unicode.IsLetter(c) && c != '.' {
75 | argType = Y2KString
76 | }
77 | }
78 |
79 | // Command line variables are added to the end of the map, which depends on
80 | // the number of digits that are parsed at one time (a parsing size of 1
81 | // should insert variables from 9->8->etc, a parsing size of 2 should insert
82 | // from 99->98->etc.)
83 | mapInd, _ := strconv.Atoi(strings.Repeat("9", y2k.Digits))
84 | for VarMap[uint8(mapInd)] != nil {
85 | mapInd -= 1
86 | }
87 |
88 | // Finalize and insert the new var into the previously determined index
89 | VarMap[uint8(mapInd)] = &Y2KVar{
90 | ID: uint8(mapInd),
91 | Size: uint8(len(input)),
92 | strVal: input,
93 | numVal: utils.StrToFloat(input),
94 | Type: argType,
95 | }
96 | }
97 |
98 | // ParseVariable recursively builds a new Y2KVar to insert into the global
99 | // variable map.
100 | // The variable creation process follows a specific order:
101 | //
102 | // start creation -> set ID -> set type -> set size -> read values
103 | //
104 | // So to create a numeric variable with the value 100 and an ID of 1, the
105 | // chain of values would need to be:
106 | //
107 | // 3 1 2 3 1 0 0
108 | func (y2k Y2K) ParseVariable(timestamp string, val reflect.Value) string {
109 | newVar := val.Interface().(Y2KVar)
110 | input := timestamp[:y2k.Digits]
111 |
112 | y2k.DebugMsg("ParseVariable: [%s]%s",
113 | input,
114 | timestamp[y2k.Digits:],
115 | )
116 |
117 | // Regardless of data type, var values are created as a string first, in
118 | // order to sequentially create the variable value across multiple passes
119 | // of the parser (i.e. 100 has to be split between multiple passes, so "1"
120 | // is added first, then "0", then the last "0", then converted to an
121 | // integer).
122 | if newVar.Type == Y2KString {
123 | input = string(utils.Printable[utils.StrToInt(input)])
124 | }
125 | newVar.strVal += input
126 |
127 | if len(newVar.strVal) >= int(newVar.Size) {
128 | newVar.strVal = newVar.strVal[:newVar.Size]
129 |
130 | if newVar.Type == Y2KVarCopy {
131 | copyVar := GetVar(uint8(utils.StrToInt(newVar.strVal)))
132 | newVar.Type = copyVar.Type
133 | newVar.Size = copyVar.Size
134 | newVar.numVal = copyVar.numVal
135 | newVar.strVal = copyVar.strVal
136 | } else {
137 | // Init numeric value of variable
138 | if newVar.Type == Y2KFloat {
139 | // First digit of a float is where the decimal should be placed
140 | decimalIndex := utils.StrToInt(newVar.strVal[0:1])
141 | newVar.strVal = newVar.strVal[1:decimalIndex+1] +
142 | "." +
143 | newVar.strVal[decimalIndex+1:]
144 | }
145 |
146 | newVar.numVal = utils.StrToFloat(newVar.strVal)
147 | }
148 |
149 | // Insert finished variable into variable map
150 | VarMap[newVar.ID] = &newVar
151 |
152 | // Return handling of the parser back to Parse
153 | return timestamp
154 | }
155 |
156 | return y2k.ParseVariable(timestamp[y2k.Digits:], reflect.ValueOf(newVar))
157 | }
158 |
--------------------------------------------------------------------------------
/src/utils/raw.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "strings"
9 | "time"
10 | )
11 |
12 | var commentChar = "#"
13 |
14 | // ReadY2KRawFile reads file contents of a Y2K file instead of file timestamps.
15 | // The syntax for these files should typically follow the following structure:
16 | // :
17 | //
18 | // For example, to print the letter "a", you could write:
19 | // 921 : Print "a"
20 | //
21 | // The file is read line by line, and whitespace and comments are ignored, so
22 | // Y2K programs can take up as much space as needed to make sense without
23 | // impacting the interpreter.
24 | func ReadY2KRawFile(file string) string {
25 | timestamp := ""
26 | raw, err := os.Open(file)
27 | Check(err)
28 |
29 | defer func(raw *os.File) {
30 | err := raw.Close()
31 | Check(err)
32 | }(raw)
33 |
34 | // Strip all whitespace and comments from file
35 | scanner := bufio.NewScanner(raw)
36 | for scanner.Scan() {
37 | line := scanner.Text()
38 |
39 | // Remove any comments from line
40 | commentIndex := strings.Index(line, commentChar)
41 | if commentIndex >= 0 {
42 | line = line[:commentIndex]
43 | }
44 |
45 | // Remove extra whitespace
46 | line = strings.ReplaceAll(line, " ", "")
47 |
48 | // Append to timestamp
49 | timestamp += line
50 | }
51 |
52 | return timestamp
53 | }
54 |
55 | // WriteFileTimestamp creates an empty file at /.y2k and modifies
56 | // the file's timestamp with the value provided.
57 | func WriteFileTimestamp(timestamp string, path string, fileNum int) {
58 | filename := fmt.Sprintf("%s/%d.y2k", path, fileNum)
59 | file, err := os.Create(filename)
60 | Check(err)
61 |
62 | err = file.Close()
63 | Check(err)
64 |
65 | // Prepend a digit for all file timestamps after the first file. The reason
66 | // for this is explained in the README.
67 | if fileNum > 0 {
68 | timestamp = "8" + timestamp
69 | }
70 |
71 | if len(timestamp) != 18 {
72 | panic("Error: Invalid timestamp length -- must be 18 chars long")
73 | }
74 |
75 | fileTime := time.Unix(int64(StrToInt(timestamp[:9])), int64(StrToInt(timestamp[9:])))
76 |
77 | fmt.Println(fmt.Sprintf("Writing %s -- %s (%s)", filename, timestamp, fileTime))
78 |
79 | err = os.Chtimes(filename, fileTime, fileTime)
80 | Check(err)
81 | }
82 |
83 | // ExportRawToTimestampFiles takes the timestamp created from a raw Y2K file
84 | // and outputs a set of empty files that have their timestamps modified to
85 | // perform the same operations as the raw file.
86 | func ExportRawToTimestampFiles(timestamp string, path string) {
87 | files := 0
88 |
89 | // Ensure path exists, and create it if not
90 | if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
91 | err := os.Mkdir(path, os.ModePerm)
92 | Check(err)
93 | }
94 |
95 | for len(timestamp) > 0 {
96 | maxLen := 17
97 | if files == 0 {
98 | maxLen = 18
99 | }
100 |
101 | // Ensure the timestamp has trailing 0s (not leading, which would
102 | // impact multi-file commands) if it's shorter than the maximum
103 | // length. This typically happens when programs require only part
104 | // of an additional file's timestamp to work properly.
105 | for len(timestamp) < maxLen {
106 | timestamp += "0"
107 | }
108 |
109 | WriteFileTimestamp(timestamp[:maxLen], path, files)
110 | timestamp = timestamp[maxLen:]
111 | files += 1
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "sort"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | var Y2KExt = ".y2k"
13 | var Printable = " abcdefghijklmnopqrstuvwxyz" +
14 | "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
15 | "1234567890" +
16 | "!@#$%^&*()+-<>.,"
17 | var MaxTimestamp = int64(999999999999999999)
18 | var StrTerm = " "
19 | var LoopTerm = "1999"
20 | var CondTerm = "2000"
21 | var ContinueCmd = "continue"
22 | var DebugDivider = "=============================="
23 |
24 | func GetFileModTime(path string, zeroPad bool) string {
25 | info, err := os.Stat(path)
26 |
27 | if err == nil {
28 | prefix := ""
29 | if zeroPad {
30 | prefix = "0"
31 | }
32 | timestamp := info.ModTime().UnixNano()
33 | if timestamp > MaxTimestamp {
34 | // File was created after the year 2000, which isn't possible,
35 | // so let's ignore it
36 | return ""
37 | }
38 |
39 | return fmt.Sprintf(prefix+"%d", timestamp)
40 | }
41 |
42 | return ""
43 | }
44 |
45 | func GetCondTerm(loop bool) string {
46 | if loop {
47 | return LoopTerm
48 | }
49 |
50 | return CondTerm
51 | }
52 |
53 | func StrToInt(input string) int {
54 | numVal, err := strconv.Atoi(input)
55 | if err != nil {
56 | return 0
57 | }
58 |
59 | return numVal
60 | }
61 |
62 | func StrToFloat(input string) float64 {
63 | numVal, err := strconv.ParseFloat(input, 64)
64 | if err != nil {
65 | return 0
66 | }
67 |
68 | return numVal
69 | }
70 |
71 | func FloatToString(input float64) string {
72 | return strconv.FormatFloat(input, 'f', -1, 64)
73 | }
74 |
75 | func StrArrToInt(input []string) int {
76 | numVal, err := strconv.Atoi(strings.Join(input, ""))
77 | if err != nil {
78 | return 0
79 | }
80 |
81 | return numVal
82 | }
83 |
84 | func StrArrToFloat(input []string) float64 {
85 | numVal, err := strconv.ParseFloat(strings.Join(input, ""), 64)
86 | if err != nil {
87 | return 0
88 | }
89 |
90 | return numVal
91 | }
92 |
93 | func StrArrToPrintable(input []string) string {
94 | output := ""
95 | for _, val := range input {
96 | index := StrToInt(val)
97 | if index < len(Printable) {
98 | output += string(Printable[index])
99 | }
100 | }
101 |
102 | return output
103 | }
104 |
105 | func SplitStrByN(input string, n int) []string {
106 | var output []string
107 |
108 | for len(input) != 0 && n < len(input) {
109 | output = append(output, input[:n])
110 | input = input[n:]
111 | }
112 |
113 | output = append(output, input)
114 | return output
115 | }
116 |
117 | func GetFileTimestamp(file string, digits int) string {
118 | // Check to see if this file is a timestamp-only file (which is the case
119 | // if GetFileModTime finds a timestamp pre-2000) or if it's a "raw" file
120 | fileModTime := GetFileModTime(file, digits > 1)
121 |
122 | if len(fileModTime) > 0 {
123 | return fileModTime
124 | }
125 |
126 | // File was made after 2000, so we can assume it's likely a raw file
127 | return ReadY2KRawFile(file)
128 | }
129 |
130 | func GetTimestamps(dir string, digits int) string {
131 | var fullTimestamp = ""
132 | files, err := os.ReadDir(dir)
133 |
134 | // If the input is not a directory, try reading it as a file
135 | if err != nil {
136 | return GetFileTimestamp(dir, digits)
137 | }
138 |
139 | directoryPath, _ := filepath.Abs(dir)
140 |
141 | // Sort contents of the specified directory by name.
142 | // Y2K files should be named in an easily sortable manner when creating
143 | // programs (i.e. 00.y2k -> 01.y2k -> etc).
144 | sort.Slice(files, func(i, j int) bool {
145 | return files[i].Name() < files[j].Name()
146 | })
147 |
148 | for _, file := range files {
149 | // Ignore any non *.y2k files
150 | if !strings.HasSuffix(file.Name(), Y2KExt) {
151 | continue
152 | }
153 |
154 | // Append timestamp to slice
155 | fullPath := filepath.Join(directoryPath, file.Name())
156 | timestamp := GetFileModTime(fullPath, digits > 1)
157 | if len(fullTimestamp) != 0 && len(timestamp) > 0 {
158 | // Snip off the leading digit for all timestamps except
159 | // the first one. We do this to avoid issues with commands
160 | // spanning across multiple files, where the next desired
161 | // digit might be a "0" (which would be ignored in a timestamp)
162 | timestamp = timestamp[digits:]
163 | }
164 | fullTimestamp += timestamp
165 | }
166 |
167 | return fullTimestamp
168 | }
169 |
170 | func Check(err error) {
171 | if err != nil {
172 | panic(err)
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | SKIP_TEST="SKIP_TEST"
6 | SCRIPT_DIR="$(CDPATH= command cd -- "$(dirname -- "$0")" && pwd -P)"
7 | TEST_DIR="$SCRIPT_DIR/test-output"
8 |
9 | echo "- Building executable"
10 | go build
11 |
12 | echo "- Running tests"
13 | for example in examples/*; do
14 | # Set up test directory for raw Y2K file exports
15 | rm -rf "$TEST_DIR"
16 | mkdir "$TEST_DIR"
17 |
18 | # Skip tests that are contain the SKIP_TEST string.
19 | # This is done for examples like "count-up-forever.y2k"
20 | if grep -q $SKIP_TEST $example; then
21 | continue
22 | fi
23 |
24 | # Evaluate the expected output of a Y2K example file
25 | expected="$(./y2k $example 15)"
26 |
27 | # Export the raw file to a set of empty timestamp files
28 | ./y2k -outdir $TEST_DIR -export $example >/dev/null
29 | output="$(./y2k $TEST_DIR 15)"
30 |
31 | # Check if both outputs are equal
32 | if [ "$output" != "$expected" ]; then
33 | echo "ERROR: $example"
34 | echo "Expected: $expected"
35 | echo "Output: $output"
36 | exit 1
37 | else
38 | echo "OK: $example"
39 | fi
40 | done
41 |
42 | echo "All tests passed"
43 | rm -rf "$TEST_DIR"
44 |
--------------------------------------------------------------------------------