├── .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 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
CommandDescriptionID
PRINTPrint variable or string9
CREATECreate a new variable8
MODIFYModify an existing variable7
CONDITIONCreate a condition6
METAModify interpreter state5
CONTINUEContinue (in loop)4
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 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 78 | 79 | 96 | 97 | 98 | 99 | 119 | 120 | 121 | 122 | 140 | 141 | 142 | 143 | 156 | 157 |
Command IDFields
9 (PRINT) 67 |
    68 |
  1. Type
  2. 69 |
      70 |
    • 1 --> String
    • 71 |
    • 2 --> Variable
    • 72 |
    73 |
  3. Size
  4. 74 |
75 |
8 (CREATE) 80 |
    81 |
  1. Variable ID
  2. 82 |
  3. Type
  4. 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 |
  5. Size
  6. 94 |
95 |
7 (MODIFY) 100 |
    101 |
  1. Variable ID
  2. 102 |
  3. Function
  4. 103 |
      104 |
    • 1 --> +=
    • 105 |
    • 2 --> -=
    • 106 |
    • 3 --> *=
    • 107 |
    • 4 --> /=
    • 108 |
    • 5 --> **= (exponentiation)
    • 109 |
    • 9 --> =
    • 110 |
    111 |
  5. Argument Is Variable
  6. 112 |
      113 |
    • 0 --> No
    • 114 |
    • 1 --> Yes (value will be treated as a variable ID)
    • 115 |
    116 |
  7. Argument Size
  8. 117 |
118 |
6 (CONDITION) 123 |
    124 |
  1. Variable ID
  2. 125 |
  3. Comparison
  4. 126 |
      127 |
    • 1 --> ==
    • 128 |
    • 2 --> <
    • 129 |
    • 3 --> >
    • 130 |
    • 4 --> Is evenly divisible by
    • 131 |
    132 |
  5. Loop
  6. 133 |
      134 |
    • 0 --> if
    • 135 |
    • 1 --> while
    • 136 |
    137 |
  7. Argument Size
  8. 138 |
139 |
5 (META) 144 |
    145 |
  1. Debug Mode
  2. 146 |
      147 |
    • 0 --> Off
    • 148 |
    • 1 --> On
    • 149 |
    150 |
  3. # of digits
  4. 151 |
      152 |
    • Updates the number of digits parsed on each pass of the interpreter
    • 153 |
    154 |
155 |
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 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 |
NumberCharacterNumberCharacter
0 (whitespace)
1a27A
2b28B
3c29C
4d30D
5e31E
6f32F
7g33G
8h34H
9i35I
10j36J
11k36K
12l37L
13m38M
14n39N
15o40O
16p41P
17q42Q
18r43R
19s44S
20t45T
21u46U
22v47V
23w48W
24x49X
25y50Y
26z51Z
352 | 353 | #### Numeric 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 |
NumberCharacter
521
532
543
554
565
576
587
598
609
610
401 | 402 | #### Symbols 403 | 404 | !@#$%^&*()+-<>., 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 |
NumberCharacter
62!
63@
64#
65$
66%
67^
68&
69*
70(
71)
72+
73-
74<
75>
76.
77,
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 |
2 | 3 | [![Y2K Logo](https://benbusby.com/assets/images/y2k.gif)](https://benbusby.com/assets/images/y2k.gif) 4 | 5 | [![MPL License](https://img.shields.io/github/license/benbusby/y2k)](LICENSE) 6 | [![GitHub release](https://img.shields.io/github/v/release/benbusby/y2k)](https://github.com/benbusby/y2k/releases) 7 | [![builds.sr.ht status](https://builds.sr.ht/~benbusby/y2k.svg)](https://builds.sr.ht/~benbusby/y2k?) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/benbusby/y2k)](https://goreportcard.com/report/github.com/benbusby/y2k) 9 |
10 | 11 | ___ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
SourceHutGitHub
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 | --------------------------------------------------------------------------------