├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENCE ├── README.md ├── build.zig ├── build.zig.zon ├── doc └── screenshot.png ├── example ├── README.md ├── example.png ├── example.txt └── png.spec └── src ├── ast.zig └── main.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: MasterQ32 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Felix "xq" Queißner 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 19 | OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # livedecode 2 | 3 | Decode binary data on the fly quicker. 4 | 5 | ## Usage 6 | 7 | ```sh-session 8 | [user@host project]$ livedecode example/png.spec example/example.png 9 | ``` 10 | 11 | The tool is meant to be run live while typing on a spec, so one can do stuff like this: 12 | ![screenshot of program usage](doc/screenshot.png) 13 | 14 | Just run the program periodically in the background: 15 | 16 | ```sh-session 17 | [user@host project]$ while true; do 18 | clear 19 | date 20 | livedecode docs/wmb6.spec data/wmb/wmb6/block.wmb > /tmp/dump.txt 21 | sleep 1 22 | done 23 | ``` 24 | 25 | ## Syntax 26 | 27 | The format is a very crude line based syntax. Empty lines are ignored, everything past a `#` is a comment. 28 | Lines are split into tokens separated by either space or tab characters. 29 | 30 | The first token in a line determines the command or type of the line. 31 | A line starting with `.` is a macro and is always executed. All other lines can be conditionally be executed. 32 | 33 | If a line starts with a type, this type is decoded. If a name is given after the type, a variable is created with that name. 34 | 35 | In a lot of places where not a name is expected, either immediate numbers can be written as decimal, hexadecimal (`0x1A`) or a variable reference can be used (`*variable`). 36 | 37 | The parser can also accept tuple types, which are started by `(` and terminated by `)`. Tuples are inhomogenous arrays that are passed as a single argument. They are used for grouping arguments together. 38 | 39 | Lines can be continued with a `\`, so the following code is considered a single line by the parser: 40 | 41 | ``` 42 | lut *key \ 43 | (key value) \ 44 | (key value) \ 45 | (key value) \ 46 | (key value) \ 47 | (key value) 48 | ``` 49 | 50 | ## Commands, Macros and Variables 51 | 52 | ### Commands 53 | 54 | #### `print …` 55 | 56 | prints all arguments. if an argument is a semicolon, no space is printed after the argument. If a semicolon is last, no line feed is printed. 57 | 58 | #### `def ` 59 | 60 | creates a variable called with the value . Useful for constants or aliases 61 | 62 | #### `seek …` 63 | 64 | sums up all offsets and moves the read cursor to the absolute position 65 | 66 | #### `move …` 67 | 68 | sums up all offsets and moves the read cursor relatively. Accepts negative numbers 69 | 70 | #### `tell` 71 | 72 | prints the file cursor 73 | 74 | #### `tell 77 | 78 | #### `dump ` 79 | 80 | dumps bytes 81 | 82 | appends everything past the ! to the last created program 83 | 84 | #### `call …` 85 | 86 | invokes a program named . all arguments past that are passed as variables arg[0] to arg[n] 87 | 88 | #### `array ` 89 | 90 | Creates an array of items called . In , the first occurance of `?` will be replaced with the array index. determines the type of the array items. 91 | 92 | #### `array ` 93 | 94 | Creates an array of items called where is a sized type (str, blob, ...). In , the first occurance of `?` will be replaced with the array index. determines the type of the array items. 95 | 96 | #### `select …` 97 | 98 | Builds a variable name from and all provided s. For each key, the next `?` in the is replaced with the value of . 99 | After all `?` are resolved, a global lookup is performed and the variable with the computed name is then copied into . 100 | 101 | #### `endian le` 102 | 103 | changes integer endianess to little endian 104 | 105 | #### `endian little` 106 | 107 | changes integer endianess to little endian 108 | 109 | #### `endian be` 110 | 111 | changes integer endianess to big endian 112 | 113 | #### `endian big` 114 | 115 | changes integer endianess to big endian 116 | 117 | #### `bitread` 118 | 119 | switches to bit-reading mode 120 | 121 | #### `byteread` 122 | 123 | switches out of bit-reading mode, discarding any unread bits in the current byte 124 | 125 | #### `bitmap ` 126 | 127 | consumes a bitmap of size \* and rgb565, rgb888, bgr888, rgbx8888 or rgba8888 128 | 129 | #### `bitmap 132 | 133 | #### `lut ( ) …` 134 | 135 | Will perform a lookup on value . If matches , the following is printed. Any number of pairs can be passed. 136 | 137 | #### `divs 140 | 141 | #### `diskdump bytes and writes them into a file called . Useful to extract portions of a file. 144 | 145 | #### `findpattern …` 146 | 147 | All arguments together form a pattern. This pattern is then searched in the file from the cursor position on and each occurrence is printed with offset. 148 | 149 | Pattern components can either be a `*` for any kind of byte, or a list of `|`-separated integers that list the possibilities for this option. 150 | 151 | To make this more clear, let's consider this example: 152 | We're searching for a list of u32 that can only consist of the integer values 1, 2 or 3, but there's a unknown length marker at the start that is a 16 bit value less than 256: 153 | 154 | ```rb 155 | # Search for at least 3 items: 156 | # len item 0 item 1 item 2 157 | findpattern * 0 1|2|3 0 0 0 1|2|3 0 0 0 1|2|3 0 0 0 158 | ``` 159 | 160 | ### Macros 161 | 162 | ```rb 163 | .if # the code following this will be executed if is not 0 164 | .if # the code following this will be executed if is equals to 165 | .else # swaps the current execution condition 166 | .endif # ends a if block 167 | .loop # Repeats the following code for times. 168 | .loop # Repeats the following code for times. Writes the current index into . 169 | .endloop # Terminates the current loop 170 | .pgm # creates a new program called 171 | .endpgm # ends the current program 172 | ``` 173 | 174 | ### Types 175 | 176 | ```rb 177 | u8 # 8 bit unsigned integer 178 | u16 # 16 bit unsigned integer 179 | u32 # 32 bit unsigned integer 180 | u64 # 64 bit unsigned integer 181 | i8 # 8 bit signed integer, two's complement 182 | i16 # 16 bit signed integer, two's complement 183 | i32 # 32 bit signed integer, two's complement 184 | i64 # 64 bit signed integer, two's complement 185 | f32 # 32 bit floating point 186 | f64 # 64 bit floating point 187 | str # ascii string of bytes, displayed as string+escapes 188 | blob # binary blob of bytes, displayed as array 189 | bitblob # binary blob of bits, displayed as array of bits (only valid in bit-reading mode) 190 | bits # an unsigned integer of bits up to 64 (only valid in bit-reading mode) 191 | ``` 192 | 193 | ### Predefined variables 194 | 195 | ```sh 196 | str file.path # The full path of the current file 197 | str file.name # The file name of the current file 198 | u64 file.size # Size of the current file in bytes 199 | ``` 200 | 201 | ## Example 202 | 203 | ``` 204 | endian le 205 | u32 magic 206 | u32 type 207 | u32 offset 208 | u32 length 209 | 210 | print section 1 211 | seek *offset 212 | dump *length 213 | 214 | .if *type 10 215 | seek 0x200 216 | str 10 description 217 | .endif 218 | ``` 219 | 220 | A more complete example can be found [here](example/), which will decode a good amount of a PNG file. 221 | 222 | ## Building 223 | 224 | 1. Fetch the latest zig install (tested with `0.10.0-dev.4442+ce3ffa5e1`). 225 | 2. Invoke `zig build`. 226 | 3. Install `zig-out/bin/livedecode` into your system in a way you like it. 227 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const parser_toolkit = b.dependency("parser_toolkit", .{}); 8 | // const zig_args = b.dependency("args", .{}); 9 | 10 | const exe = b.addExecutable(.{ 11 | .name = "livedecode", 12 | .root_source_file = .{ .path = "src/main.zig" }, 13 | .target = target, 14 | .optimize = optimize, 15 | }); 16 | exe.addModule("parser-toolkit", parser_toolkit.module("parser-toolkit")); 17 | b.installArtifact(exe); 18 | 19 | const run_cmd = b.addRunArtifact(exe); 20 | run_cmd.step.dependOn(b.getInstallStep()); 21 | if (b.args) |args| { 22 | run_cmd.addArgs(args); 23 | } 24 | 25 | const run_step = b.step("run", "Run the app"); 26 | run_step.dependOn(&run_cmd.step); 27 | 28 | const exe_tests = b.addTest(.{ 29 | .root_source_file = .{ .path = "src/main.zig" }, 30 | .target = target, 31 | .optimize = optimize, 32 | }); 33 | exe_tests.addModule("parser-toolkit", parser_toolkit.module("parser-toolkit")); 34 | 35 | const test_step = b.step("test", "Run unit tests"); 36 | test_step.dependOn(&b.addRunArtifact(exe_tests).step); 37 | } 38 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = "livedecode", 3 | .version = "0.1.0", 4 | 5 | .dependencies = .{ 6 | .parser_toolkit = .{ 7 | .url = "https://github.com/MasterQ32/parser-toolkit/archive/7e7188bfaa46d0ba36d1ef31c1f368d264aa6876.tar.gz", 8 | .hash = "12202ae562efbd6a4f12f1b10c5237329d19d1be15eec7fa1b0b02f5658500bfcbfd", 9 | }, 10 | .args = .{ 11 | .url = "https://github.com/MasterQ32/zig-args/archive/bb2eced8ddf28114b3a1ff761c2d80b90b1a61e2.tar.gz", 12 | .hash = "12208556082c280af264ca2a174fd04fc7a35f865bfe59e2c61f4a977bf7a65063e4", 13 | }, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/livedecode/dddf1ca9b72971b06602b8b9e9ebf24d2ee43ea7/doc/screenshot.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # PNG example 2 | 3 | Invoke this in the main folder: 4 | 5 | ```sh-session 6 | [user@host livedecode]$ livedecode example/png.spec example/example.png 7 | "File Header:" 8 | magic_8bit = 137 9 | magic = "PNG\r\n" 10 | magic_1a = 26 11 | magic_0a = 10 12 | chunk "IHDR" 13 | chunk_length = 13 14 | type = 1229472850 15 | type_str = "IHDR" 16 | width = 45 17 | height = 24 18 | depth = 8 19 | color_type = 2 20 | compression = 0 21 | filter = 0 22 | interlace = 0 23 | crc = 3788497875 24 | chunk "gAMA" 25 | chunk_length = 4 26 | type = 1732332865 27 | type_str = "gAMA" 28 | 0x00000029: 00 00 B1 8F __ __ __ __ __ __ __ __ __ __ __ __ |.... | 29 | crc = 201089285 30 | chunk "cHRM" 31 | chunk_length = 32 32 | type = 1665684045 33 | type_str = "cHRM" 34 | 0x00000039: 00 00 7A 26 00 00 80 84 00 00 FA 00 00 00 80 E8 |..z&............| 35 | 0x00000049: 00 00 75 30 00 00 EA 60 00 00 3A 98 00 00 17 70 |..u0...`..:....p| 36 | crc = 2629456188 37 | chunk "bKGD" 38 | chunk_length = 6 39 | type = 1649100612 40 | type_str = "bKGD" 41 | 0x00000065: 00 FF 00 FF 00 FF __ __ __ __ __ __ __ __ __ __ |...... | 42 | crc = 2696783763 43 | chunk "tIME" 44 | chunk_length = 7 45 | type = 1950960965 46 | type_str = "tIME" 47 | year = 2021 48 | month = 9 49 | day = 14 50 | hour = 15 51 | minute = 5 52 | second = 55 53 | crc = 2835620772 54 | chunk "IDAT" 55 | chunk_length = 399 56 | type = 1229209940 57 | type_str = "IDAT" 58 | 0x0000008A: 48 C7 ED 94 CD 8A 54 31 10 46 4F 25 55 91 E9 5E |H.....T1.FO%U..^| 59 | 0x0000009A: 08 03 BE FF 13 3A 28 F4 BD AD 55 49 CA 45 FF 5E |.....:(...UI.E.^| 60 | 0x000000AA: 71 5C 08 82 C2 2D 0E E1 F0 A5 16 DF 26 91 5E 33 |q\...-......&.^3| 61 | 0x000000BA: 0C 6F 57 EE FE 1B F9 1B 6B 85 7F 63 F6 1E 7B 8F |.oW.....k..c..{.| 62 | 0x000000CA: BD C7 FF D7 43 53 32 85 0B 6C 85 77 44 D8 88 DC |....CS2..l.wD...| 63 | 0x000000DA: 85 8D 48 BE 23 B9 95 44 12 D5 2E 09 53 AE 54 A1 |..H.#..D....S.T.| 64 | 0x000000EA: 0A 43 28 82 14 4A B9 CA 05 3A 14 B2 90 83 1C CC |.C(..J...:......| 65 | 0x000000FA: C2 1C 8C C1 A8 8C C9 C8 2B 1D 3A 0C A1 5F 28 0F |........+.:.._(.| 66 | 0x0000010A: A2 10 95 5E 89 4A 28 A1 84 31 2A FA F6 FA 35 1A |...^.J(..1*...5.| 67 | 0x0000011A: DD 08 93 BB F4 46 3C 49 37 99 8D 34 30 29 8D 6A |.....F| 75 | 0x0000019A: 60 2F C4 4B 2A 19 74 27 82 70 AA 53 1C 71 32 98 |`/.K*.t'.p.S.q2.| 76 | 0x000001AA: CE 70 C2 F1 E0 BB 73 76 56 E7 E4 2C C1 C9 59 9C |.p....svV..,..Y.| 77 | 0x000001BA: 93 73 72 D6 A1 96 6A 98 DE A8 68 C5 2A 56 D0 0B |.sr...j...h.*V..| 78 | 0x000001CA: 82 0A 0A 2A 54 50 D0 A4 CE 3B 52 E7 AC 65 CE CC |...*TP...;R..e..| 79 | 0x000001DA: 4A 4E B2 92 97 B5 89 4C 50 64 52 26 92 94 49 99 |JN.....LPdR&..I.| 80 | 0x000001EA: D4 44 93 9A 57 D1 C4 12 BD BC 17 1E F0 24 3C C9 |.D..W........$<.| 81 | 0x000001FA: 16 E4 26 80 48 42 0A F0 08 B7 FB B9 3D F3 17 C9 |..&.HB......=...| 82 | 0x0000020A: FE 8F ED 3D F6 1E 7B 8F 3F 9D 1F 89 E8 5F 16 __ |...=..{.?...._. | 83 | crc = 2758011464 84 | chunk "tEXt" 85 | chunk_length = 37 86 | type = 1950701684 87 | type_str = "tEXt" 88 | text = "date:create\x002021-09-14T15:05:55+00:00" 89 | crc = 3937509577 90 | chunk "tEXt" 91 | chunk_length = 37 92 | type = 1950701684 93 | type_str = "tEXt" 94 | text = "date:modify\x002021-09-14T15:05:55+00:00" 95 | crc = 2615941237 96 | chunk "IEND" 97 | chunk_length = 0 98 | type = 1229278788 99 | type_str = "IEND" 100 | crc = 2923585666 101 | [user@host livedecode]$ 102 | ``` 103 | -------------------------------------------------------------------------------- /example/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikskuh/livedecode/dddf1ca9b72971b06602b8b9e9ebf24d2ee43ea7/example/example.png -------------------------------------------------------------------------------- /example/example.txt: -------------------------------------------------------------------------------- 1 | "File Header:" 2 | magic_8bit = 137 3 | magic = "PNG\r\n" 4 | magic_1a = 26 5 | magic_0a = 10 6 | chunk "IHDR" 7 | chunk_length = 13 8 | type = 1229472850 9 | 1229472850 => image header 10 | type_str = "IHDR" 11 | width = 45 12 | height = 24 13 | depth = 8 14 | color_type = 2 15 | compression = 0 16 | filter = 0 17 | interlace = 0 18 | crc = 3788497875 19 | chunk "gAMA" 20 | chunk_length = 4 21 | type = 1732332865 22 | 1732332865 => gAMA 23 | type_str = "gAMA" 24 | 0x00000029: 00 00 B1 8F __ __ __ __ __ __ __ __ __ __ __ __ |.... | 25 | crc = 201089285 26 | chunk "cHRM" 27 | chunk_length = 32 28 | type = 1665684045 29 | 1665684045 => cHRM 30 | type_str = "cHRM" 31 | 0x00000039: 00 00 7A 26 00 00 80 84 00 00 FA 00 00 00 80 E8 |..z&............| 32 | 0x00000049: 00 00 75 30 00 00 EA 60 00 00 3A 98 00 00 17 70 |..u0...`..:....p| 33 | crc = 2629456188 34 | chunk "bKGD" 35 | chunk_length = 6 36 | type = 1649100612 37 | 1649100612 => bKGD 38 | type_str = "bKGD" 39 | 0x00000065: 00 FF 00 FF 00 FF __ __ __ __ __ __ __ __ __ __ |...... | 40 | crc = 2696783763 41 | chunk "tIME" 42 | chunk_length = 7 43 | type = 1950960965 44 | 1950960965 => creation time 45 | type_str = "tIME" 46 | year = 2021 47 | month = 9 48 | day = 14 49 | hour = 15 50 | minute = 5 51 | second = 55 52 | crc = 2835620772 53 | chunk "IDAT" 54 | chunk_length = 399 55 | type = 1229209940 56 | 1229209940 => IDAT 57 | type_str = "IDAT" 58 | 0x0000008A: 48 C7 ED 94 CD 8A 54 31 10 46 4F 25 55 91 E9 5E |H.....T1.FO%U..^| 59 | 0x0000009A: 08 03 BE FF 13 3A 28 F4 BD AD 55 49 CA 45 FF 5E |.....:(...UI.E.^| 60 | 0x000000AA: 71 5C 08 82 C2 2D 0E E1 F0 A5 16 DF 26 91 5E 33 |q\...-......&.^3| 61 | 0x000000BA: 0C 6F 57 EE FE 1B F9 1B 6B 85 7F 63 F6 1E 7B 8F |.oW.....k..c..{.| 62 | 0x000000CA: BD C7 FF D7 43 53 32 85 0B 6C 85 77 44 D8 88 DC |....CS2..l.wD...| 63 | 0x000000DA: 85 8D 48 BE 23 B9 95 44 12 D5 2E 09 53 AE 54 A1 |..H.#..D....S.T.| 64 | 0x000000EA: 0A 43 28 82 14 4A B9 CA 05 3A 14 B2 90 83 1C CC |.C(..J...:......| 65 | 0x000000FA: C2 1C 8C C1 A8 8C C9 C8 2B 1D 3A 0C A1 5F 28 0F |........+.:.._(.| 66 | 0x0000010A: A2 10 95 5E 89 4A 28 A1 84 31 2A FA F6 FA 35 1A |...^.J(..1*...5.| 67 | 0x0000011A: DD 08 93 BB F4 46 3C 49 37 99 8D 34 30 29 8D 6A |.....F| 75 | 0x0000019A: 60 2F C4 4B 2A 19 74 27 82 70 AA 53 1C 71 32 98 |`/.K*.t'.p.S.q2.| 76 | 0x000001AA: CE 70 C2 F1 E0 BB 73 76 56 E7 E4 2C C1 C9 59 9C |.p....svV..,..Y.| 77 | 0x000001BA: 93 73 72 D6 A1 96 6A 98 DE A8 68 C5 2A 56 D0 0B |.sr...j...h.*V..| 78 | 0x000001CA: 82 0A 0A 2A 54 50 D0 A4 CE 3B 52 E7 AC 65 CE CC |...*TP...;R..e..| 79 | 0x000001DA: 4A 4E B2 92 97 B5 89 4C 50 64 52 26 92 94 49 99 |JN.....LPdR&..I.| 80 | 0x000001EA: D4 44 93 9A 57 D1 C4 12 BD BC 17 1E F0 24 3C C9 |.D..W........$<.| 81 | 0x000001FA: 16 E4 26 80 48 42 0A F0 08 B7 FB B9 3D F3 17 C9 |..&.HB......=...| 82 | 0x0000020A: FE 8F ED 3D F6 1E 7B 8F 3F 9D 1F 89 E8 5F 16 __ |...=..{.?...._. | 83 | crc = 2758011464 84 | chunk "tEXt" 85 | chunk_length = 37 86 | type = 1950701684 87 | 1950701684 => text extension 88 | type_str = "tEXt" 89 | text = "date:create\x002021-09-14T15:05:55+00:00" 90 | crc = 3937509577 91 | chunk "tEXt" 92 | chunk_length = 37 93 | type = 1950701684 94 | 1950701684 => text extension 95 | type_str = "tEXt" 96 | text = "date:modify\x002021-09-14T15:05:55+00:00" 97 | crc = 2615941237 98 | chunk "IEND" 99 | chunk_length = 0 100 | type = 1229278788 101 | 1229278788 => end of file 102 | type_str = "IEND" 103 | crc = 2923585666 104 | -------------------------------------------------------------------------------- /example/png.spec: -------------------------------------------------------------------------------- 1 | 2 | endian be 3 | print "File Header:" 4 | 5 | u8 magic_8bit # should be 0x89 6 | str 5 magic # PNG\r\n 7 | u8 magic_1a # 0x1A 8 | u8 magic_0a # 0x0a 9 | 10 | def tEXt 1950701684 11 | def IHDR 1229472850 12 | def tIME 1950960965 13 | 14 | call chunk "IHDR" 15 | call chunk "gAMA" 16 | call chunk "cHRM" 17 | call chunk "bKGD" 18 | call chunk "tIME" 19 | call chunk "IDAT" 20 | call chunk "tEXt" 21 | call chunk "tEXt" 22 | call chunk "IEND" 23 | 24 | 25 | 26 | .pgm chunk 27 | print chunk *arg[0] 28 | u32 chunk_length 29 | u32 type 30 | lut *type \ 31 | (*tEXt "text extension") \ 32 | (*IHDR "image header") \ 33 | (*tIME "creation time") \ 34 | (1732332865 gAMA) \ 35 | (1665684045 cHRM) \ 36 | (1649100612 bKGD) \ 37 | (1229209940 IDAT) \ 38 | (1229278788 "end of file") 39 | move -4 40 | str 4 type_str 41 | .if *type *tEXt 42 | str *chunk_length text 43 | .elseif *type *IHDR 44 | u32 width 45 | u32 height 46 | u8 depth 47 | u8 color_type 48 | u8 compression 49 | u8 filter 50 | u8 interlace 51 | .elseif *type *tIME 52 | u16 year 53 | u8 month 54 | u8 day 55 | u8 hour 56 | u8 minute 57 | u8 second 58 | .else 59 | dump *chunk_length 60 | .endif 61 | u32 crc 62 | .endpgm -------------------------------------------------------------------------------- /src/ast.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ptk = @import("parser-toolkit"); 3 | const mainfile = @import("main.zig"); 4 | 5 | const allo = mainfile.allo; 6 | 7 | pub const Script = struct { 8 | top_level: Sequence, 9 | programs: []Program, 10 | }; 11 | 12 | pub const Sequence = struct { 13 | is_top_level: bool, 14 | instructions: []Node, 15 | }; 16 | 17 | pub const Node = union(enum) { 18 | command: Command, 19 | decision: Conditional, 20 | loop: Loop, 21 | breakloop, 22 | }; 23 | 24 | pub const Program = struct { 25 | name: []const u8, 26 | code: Sequence, 27 | }; 28 | 29 | pub const Conditional = struct { 30 | value: ValueToken, 31 | comparison: ?ValueToken, 32 | 33 | true_body: Sequence, 34 | false_body: ?Sequence, 35 | }; 36 | 37 | pub const Loop = struct { 38 | count: ValueToken, 39 | variable: ?[]const u8, 40 | 41 | body: Sequence, 42 | }; 43 | 44 | pub const Command = struct { 45 | name: []const u8, 46 | arguments: []ValueToken, 47 | }; 48 | 49 | pub const ValueToken = union(enum) { 50 | number: []const u8, // 10, 24.5 51 | variable_ref: []const u8, // *foo 52 | string: []const u8, // "hello, world"- 53 | tuple: []ValueToken, // ( ) 54 | identifier: []const u8, // the rest: hello, foo_bar 55 | }; 56 | 57 | const TokenType = enum { 58 | macro, 59 | identifier, 60 | @"(", 61 | @")", 62 | star_ref, 63 | string, 64 | number_literal, 65 | line_feed, 66 | whitespace, 67 | comment, 68 | }; 69 | 70 | const Pattern = ptk.Pattern(TokenType); 71 | const Token = Tokenizer.Token; 72 | 73 | const whitespace_chars = " \t"; 74 | 75 | const identifier_matcher = ptk.matchers.takeNoneOf(whitespace_chars ++ ")(\r\n"); 76 | 77 | const Tokenizer = ptk.Tokenizer(TokenType, &.{ 78 | // allow escaping of a line feed to be considered whitespace => easy line continuation 79 | Pattern.create(.whitespace, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("\\"), ptk.matchers.linefeed })), 80 | Pattern.create(.line_feed, ptk.matchers.linefeed), 81 | Pattern.create(.comment, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("#"), ptk.matchers.takeNoneOf("\n") })), 82 | Pattern.create(.@"(", ptk.matchers.literal("(")), 83 | Pattern.create(.@")", ptk.matchers.literal(")")), 84 | Pattern.create(.string, matchString), 85 | Pattern.create(.macro, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("."), identifier_matcher })), 86 | Pattern.create(.star_ref, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("*"), identifier_matcher })), 87 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("0b"), ptk.matchers.binaryNumber })), 88 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("0x"), ptk.matchers.hexadecimalNumber })), 89 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("0o"), ptk.matchers.octalNumber })), 90 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("-"), ptk.matchers.decimalNumber, ptk.matchers.literal("."), ptk.matchers.decimalNumber })), 91 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.literal("-"), ptk.matchers.decimalNumber })), 92 | Pattern.create(.number_literal, ptk.matchers.sequenceOf(.{ ptk.matchers.decimalNumber, ptk.matchers.literal("."), ptk.matchers.decimalNumber })), 93 | Pattern.create(.number_literal, ptk.matchers.decimalNumber), 94 | Pattern.create(.identifier, identifier_matcher), 95 | Pattern.create(.whitespace, ptk.matchers.takeAnyOf(whitespace_chars)), 96 | }); 97 | 98 | fn matchString(string: []const u8) ?usize { 99 | if (string.len < 2) return null; 100 | if (string[0] != '"') return null; 101 | 102 | var i: usize = 1; 103 | while (i < string.len) { 104 | if (string[i] == '"') { 105 | return i + 1; 106 | } 107 | if (string[i] == '\\') { 108 | if (i + 1 == string.len) return null; 109 | i += 1; 110 | } 111 | i += 1; 112 | } 113 | 114 | return null; 115 | } 116 | 117 | pub fn loadFile(file_name: []const u8) Script { 118 | const source_code = std.fs.cwd().readFileAlloc(allo, file_name, 1 << 20) catch @panic("oom"); 119 | 120 | return load(source_code, file_name); 121 | } 122 | 123 | pub fn load(source_code: []const u8, file_name: ?[]const u8) Script { 124 | var tokenizer = Tokenizer.init(source_code, file_name); 125 | 126 | // Script 127 | 128 | var script = Script{ 129 | .programs = allo.alloc(Program, 0) catch @panic("oom"), 130 | .top_level = undefined, 131 | }; 132 | script.top_level = parseSequence(&script, &tokenizer, null); 133 | return script; 134 | } 135 | 136 | // pub fn render(script: Script, stream: anytype) @TypeOf(stream).Error!void { 137 | // for (script.programs) |prog| { 138 | // try stream.print(".pgm {s}\n", .{prog.name}); 139 | // try renderSeq(1, prog.code, stream); 140 | // try stream.writeAll(".endpgm\n\n"); 141 | // } 142 | 143 | // try renderSeq(0, script.top_level, stream); 144 | // } 145 | 146 | // fn wrindent(indent: usize, stream: anytype) !void { 147 | // try stream.writeByteNTimes(' ', indent); 148 | // } 149 | 150 | // fn renderSeq(indent: usize, seq: Sequence, stream: anytype) @TypeOf(stream).Error!void { 151 | // for (seq.instructions) |instr| { 152 | // switch (instr) { 153 | // // 154 | // } 155 | // } 156 | // } 157 | 158 | fn next(stream: *Tokenizer) ?Token { 159 | while (true) { 160 | var t = (stream.next() catch std.debug.panic("invalid token: '{}'", .{std.zig.fmtEscapes(stream.source[stream.offset..])})) orelse return null; 161 | 162 | switch (t.type) { 163 | .whitespace, .comment => {}, 164 | else => return t, 165 | } 166 | } 167 | } 168 | 169 | fn isStr(a: Token, b: []const u8) bool { 170 | return std.mem.eql(u8, a.text, b); 171 | } 172 | 173 | fn fetchId(stream: *Tokenizer) []const u8 { 174 | const id = next(stream) orelse @panic("identifier expected"); 175 | if (id.type != .identifier) @panic("identifier expected"); 176 | return id.text; 177 | } 178 | 179 | fn endOfLine(stream: *Tokenizer) void { 180 | const id = next(stream) orelse return; 181 | if (id.type != .line_feed) 182 | @panic("Expected end of line!"); 183 | } 184 | 185 | const Macro = enum { 186 | loop, 187 | endloop, 188 | 189 | pgm, 190 | endpgm, 191 | 192 | @"if", 193 | elseif, 194 | @"else", 195 | endif, 196 | }; 197 | 198 | fn makeSingleTerminator(comptime mac: Macro) fn (Macro) bool { 199 | return struct { 200 | fn f(t: Macro) bool { 201 | return t == mac; 202 | } 203 | }.f; 204 | } 205 | 206 | var if_terminator: Macro = undefined; 207 | fn isEndOrElseOrElseIf(m: Macro) bool { 208 | if_terminator = m; 209 | return m == .endif or m == .elseif or m == .@"else"; 210 | } 211 | 212 | fn parseConditionBlock(stream: *Tokenizer) Conditional { 213 | const value = parseTokenValue(stream, next(stream) orelse @panic("expected condition value")); 214 | 215 | const maybe_comp = next(stream); 216 | 217 | const comparison = if (maybe_comp != null and maybe_comp.?.type != .line_feed) 218 | parseTokenValue(stream, maybe_comp.?) 219 | else 220 | null; 221 | 222 | if (comparison != null) 223 | endOfLine(stream); // terminate .if or .elseif 224 | 225 | const true_block = parseSequence(undefined, stream, isEndOrElseOrElseIf); 226 | 227 | if (if_terminator == .endif) { 228 | endOfLine(stream); // terminate .endif 229 | return Conditional{ 230 | .value = value, 231 | .comparison = comparison, 232 | .true_body = true_block, 233 | .false_body = null, 234 | }; 235 | } 236 | if (if_terminator == .@"else") { 237 | endOfLine(stream); // terminate .else 238 | const false_block = parseSequence(undefined, stream, isEndOrElseOrElseIf); 239 | endOfLine(stream); // terminate .endif 240 | 241 | return Conditional{ 242 | .value = value, 243 | .comparison = comparison, 244 | .true_body = true_block, 245 | .false_body = false_block, 246 | }; 247 | } 248 | 249 | if (if_terminator == .elseif) { 250 | // transform .elseif into a 251 | // 252 | // .else 253 | // .if new … 254 | // .endif 255 | // .endif # this is implicit from the previous one 256 | const false_block = parseConditionBlock(stream); 257 | 258 | const seq = allo.alloc(Node, 1) catch @panic("oom"); 259 | seq[0] = Node{ .decision = false_block }; 260 | 261 | return Conditional{ 262 | .value = value, 263 | .comparison = comparison, 264 | .true_body = true_block, 265 | .false_body = Sequence{ .instructions = seq, .is_top_level = false }, 266 | }; 267 | } 268 | std.debug.panic("invalid terminator: {}\n", .{if_terminator}); 269 | } 270 | 271 | fn parseSequence(script: *Script, stream: *Tokenizer, terminator: ?*const fn (Macro) bool) Sequence { 272 | var list = std.ArrayList(Node).init(allo); 273 | defer list.deinit(); 274 | 275 | while (true) { 276 | const first_token: Token = next(stream) orelse if (terminator == null) 277 | return Sequence{ .is_top_level = true, .instructions = list.toOwnedSlice() catch @panic("out of memory") } 278 | else 279 | std.debug.panic("Did not expect end of script.", .{}); 280 | 281 | switch (first_token.type) { 282 | .macro => { 283 | const mac = std.meta.stringToEnum(Macro, first_token.text[1..]) orelse std.debug.panic("Unknown macro {s}", .{first_token.text}); 284 | 285 | if (terminator != null and terminator.?(mac)) { 286 | return Sequence{ 287 | .is_top_level = false, 288 | .instructions = list.toOwnedSlice() catch @panic("oom"), 289 | }; 290 | } 291 | 292 | switch (mac) { 293 | .@"if" => { 294 | var block = parseConditionBlock(stream); 295 | list.append(Node{ .decision = block }) catch @panic("oom"); 296 | }, 297 | 298 | .loop => { 299 | const count = parseTokenValue(stream, next(stream) orelse @panic("expected loop counter!")); 300 | 301 | const var_or_term = next(stream) orelse Token{ .type = .line_feed, .text = "\n", .location = stream.current_location }; 302 | const variable = switch (var_or_term.type) { 303 | .line_feed => null, 304 | .identifier => var_or_term.text, 305 | else => @panic("unexpected token"), 306 | }; 307 | if (variable != null) 308 | endOfLine(stream); 309 | 310 | var seq = parseSequence(undefined, stream, makeSingleTerminator(.endloop)); 311 | 312 | endOfLine(stream); 313 | 314 | list.append(Node{ 315 | .loop = Loop{ 316 | .count = count, 317 | .variable = variable, 318 | .body = seq, 319 | }, 320 | }) catch @panic("oom"); 321 | }, 322 | 323 | .pgm => { 324 | if (terminator != null) 325 | @panic("Nested .pgm is not allowed"); 326 | 327 | var pgm = Program{ 328 | .name = fetchId(stream), 329 | .code = undefined, 330 | }; 331 | endOfLine(stream); 332 | pgm.code = parseSequence(undefined, stream, makeSingleTerminator(.endpgm)); 333 | endOfLine(stream); // .endpgm has no possible arg 334 | 335 | const new = allo.realloc(script.programs, script.programs.len + 1) catch @panic("oom"); 336 | new[new.len - 1] = pgm; 337 | script.programs = new; 338 | }, 339 | 340 | .endloop, .endpgm, .endif, .elseif, .@"else" => std.debug.print("Unexpected token {s}", .{first_token.text}), 341 | } 342 | }, 343 | 344 | // any command or type decoding 345 | .identifier => { 346 | var cmd = Command{ 347 | .name = first_token.text, 348 | .arguments = parseTokenList(stream, false), 349 | }; 350 | list.append(Node{ .command = cmd }) catch @panic("oom"); 351 | }, 352 | 353 | .@"(", .@")", .star_ref, .string, .number_literal => std.debug.panic("illegal start of command: '{s}'", .{first_token.text}), 354 | .line_feed => continue, // empty line 355 | .whitespace, .comment => unreachable, 356 | } 357 | } 358 | } 359 | 360 | fn parseTokenValue(stream: *Tokenizer, item: Token) ValueToken { 361 | return switch (item.type) { 362 | .macro, .identifier => ValueToken{ .identifier = item.text }, 363 | .number_literal => ValueToken{ .number = item.text }, 364 | .star_ref => ValueToken{ .variable_ref = item.text[1..] }, 365 | .string => ValueToken{ .string = unescapeString(item.text[1 .. item.text.len - 1]) }, 366 | .@"(" => ValueToken{ .tuple = parseTokenList(stream, true) }, 367 | .@")", .line_feed => std.debug.panic("Expected value, got {s}", .{@tagName(item.type)}), 368 | .whitespace, .comment => unreachable, 369 | }; 370 | } 371 | 372 | fn parseTokenList(stream: *Tokenizer, is_tuple: bool) []ValueToken { 373 | var items = std.ArrayList(ValueToken).init(allo); 374 | defer items.deinit(); 375 | 376 | while (true) { 377 | const item = next(stream) orelse if (is_tuple) @panic("tuple declaration is not closed") else return items.toOwnedSlice() catch @panic("oom"); 378 | 379 | const val = switch (item.type) { 380 | .@")" => if (is_tuple) 381 | return items.toOwnedSlice() catch @panic("oom") 382 | else 383 | @panic("no tuple declaration found"), 384 | .line_feed => if (is_tuple) 385 | @panic("tuple declaration is not closed") 386 | else 387 | return items.toOwnedSlice() catch @panic("oom"), 388 | 389 | else => parseTokenValue(stream, item), 390 | }; 391 | 392 | items.append(val) catch @panic("oom"); 393 | } 394 | } 395 | 396 | fn unescapeString(str: []const u8) []const u8 { 397 | // TODO: Implement this! 398 | return str; 399 | } 400 | 401 | fn runTest(source: []const u8) Script { 402 | mainfile.arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 403 | return load(source, null); 404 | } 405 | 406 | test "empty file" { 407 | _ = runTest(""); 408 | } 409 | 410 | test "only empty lines" { 411 | _ = runTest("\n\n\n\r\n\r\n\r\n \r\n \r\n\t\t\r\n"); 412 | } 413 | 414 | test "comments and empty lines" { 415 | _ = runTest( 416 | \\# hello 417 | \\ # hello, this is nice 418 | \\# unterminated! 419 | ); 420 | } 421 | 422 | test "commands" { 423 | _ = runTest( 424 | \\u32 425 | \\u32 # with comment 426 | \\u32 name 427 | \\u16 name 428 | \\str 16 name 429 | \\type *name 430 | \\type (tuple tuple) 431 | \\type (tuple 1 2 3) (3 4 5) 432 | \\type (*tfoo (1 2) (3 4)) 433 | \\type "hello, world" 434 | \\type ("hello, string" "foo bar") 435 | \\type "\"" "\'" "foo bar" 436 | \\type .mac .foo .bar 437 | \\ 438 | ); 439 | } 440 | 441 | test "basic loop" { 442 | const prog = runTest( 443 | \\.loop 10 444 | \\ 445 | \\.endloop 446 | ); 447 | try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 448 | try std.testing.expect(prog.top_level.instructions[0] == .loop); 449 | try std.testing.expect(prog.top_level.instructions[0].loop.variable == null); 450 | try std.testing.expect(prog.top_level.instructions[0].loop.body.instructions.len == 0); 451 | } 452 | 453 | test "nested basic loop" { 454 | const prog = runTest( 455 | \\.loop 10 456 | \\ .loop 20 457 | \\ .endloop 458 | \\.endloop 459 | ); 460 | try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 461 | try std.testing.expect(prog.top_level.instructions[0] == .loop); 462 | try std.testing.expect(prog.top_level.instructions[0].loop.variable == null); 463 | try std.testing.expect(prog.top_level.instructions[0].loop.body.instructions.len == 1); 464 | try std.testing.expect(prog.top_level.instructions[0].loop.body.instructions[0] == .loop); 465 | try std.testing.expect(prog.top_level.instructions[0].loop.body.instructions[0].loop.variable == null); 466 | try std.testing.expect(prog.top_level.instructions[0].loop.body.instructions[0].loop.body.instructions.len == 0); 467 | } 468 | 469 | test "variable loop" { 470 | const prog = runTest( 471 | \\.loop 10 index 472 | \\ 473 | \\.endloop 474 | ); 475 | try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 476 | try std.testing.expect(prog.top_level.instructions[0] == .loop); 477 | try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 478 | } 479 | 480 | test "simple if block" { 481 | const prog = runTest( 482 | \\.if foo 483 | \\ 484 | \\.endif 485 | ); 486 | _ = prog; 487 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 488 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 489 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 490 | } 491 | 492 | test "comparison if block" { 493 | const prog = runTest( 494 | \\.if foo bar 495 | \\ 496 | \\.endif 497 | ); 498 | _ = prog; 499 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 500 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 501 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 502 | } 503 | 504 | test "simple if-else" { 505 | const prog = runTest( 506 | \\.if foo 507 | \\ 508 | \\.else 509 | \\ 510 | \\.endif 511 | ); 512 | _ = prog; 513 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 514 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 515 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 516 | } 517 | 518 | test "comparison if-else" { 519 | const prog = runTest( 520 | \\.if foo bar 521 | \\ 522 | \\.else 523 | \\ 524 | \\.endif 525 | ); 526 | _ = prog; 527 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 528 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 529 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 530 | } 531 | 532 | test "simple if-elseif" { 533 | const prog = runTest( 534 | \\.if foo 535 | \\ 536 | \\.elseif bar 537 | \\ 538 | \\.elseif bam 539 | \\ 540 | \\.endif 541 | ); 542 | _ = prog; 543 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 544 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 545 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 546 | } 547 | 548 | test "comparison if-elseif" { 549 | const prog = runTest( 550 | \\.if foo 10 551 | \\ 552 | \\.elseif bar 10 553 | \\ 554 | \\.elseif bam 10 555 | \\ 556 | \\.endif 557 | ); 558 | _ = prog; 559 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 560 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 561 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 562 | } 563 | 564 | test "simple if-elseif-else" { 565 | const prog = runTest( 566 | \\.if foo 567 | \\ 568 | \\.elseif bar 569 | \\ 570 | \\.elseif bam 571 | \\ 572 | \\.else 573 | \\ 574 | \\.endif 575 | ); 576 | _ = prog; 577 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 578 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 579 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 580 | } 581 | 582 | test "comparison if-elseif-else" { 583 | const prog = runTest( 584 | \\.if foo 10 585 | \\ 586 | \\.elseif bar 10 587 | \\ 588 | \\.elseif bam 10 589 | \\ 590 | \\.else 591 | \\ 592 | \\.endif 593 | ); 594 | _ = prog; 595 | // try std.testing.expectEqual(@as(usize, 1), prog.top_level.instructions.len); 596 | // try std.testing.expect(prog.top_level.instructions[0] == .loop); 597 | // try std.testing.expectEqualStrings("index", prog.top_level.instructions[0].loop.variable.?); 598 | } 599 | 600 | test "subprograms" { 601 | const prog = runTest( 602 | \\.pgm foobar 603 | \\ type 10 604 | \\ type 20 605 | \\ type 30 606 | \\.endpgm 607 | \\ 608 | ); 609 | try std.testing.expectEqual(true, prog.top_level.is_top_level); 610 | try std.testing.expectEqual(@as(usize, 0), prog.top_level.instructions.len); 611 | try std.testing.expectEqual(@as(usize, 1), prog.programs.len); 612 | try std.testing.expectEqualStrings("foobar", prog.programs[0].name); 613 | try std.testing.expectEqual(false, prog.programs[0].code.is_top_level); 614 | try std.testing.expectEqual(@as(usize, 3), prog.programs[0].code.instructions.len); 615 | } 616 | 617 | test "line continuation" { 618 | const prog = runTest( 619 | \\lut *key \ 620 | \\ (key value) \ 621 | \\ (key value) \ 622 | \\ (key value) \ 623 | \\ (key value) \ 624 | \\ (key value) 625 | \\magic 626 | ); 627 | try std.testing.expectEqual(true, prog.top_level.is_top_level); 628 | try std.testing.expectEqual(@as(usize, 2), prog.top_level.instructions.len); 629 | try std.testing.expectEqualStrings("lut", prog.top_level.instructions[0].command.name); 630 | try std.testing.expectEqualStrings("magic", prog.top_level.instructions[1].command.name); 631 | } 632 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const ast = @import("ast.zig"); 3 | 4 | pub var arena: std.heap.ArenaAllocator = undefined; 5 | pub const allo: std.mem.Allocator = arena.allocator(); 6 | 7 | const split_chars = " \r\n\t"; 8 | 9 | test { 10 | _ = ast; 11 | } 12 | 13 | fn trim(str: []const u8) []const u8 { 14 | return std.mem.trim(u8, str, split_chars); 15 | } 16 | 17 | var bout: std.io.BufferedWriter(4096, std.fs.File.Writer) = undefined; 18 | 19 | fn write(comptime fmt: []const u8, args: anytype) void { 20 | bout.writer().print(fmt, args) catch std.os.exit(1); 21 | } 22 | 23 | pub fn main() !void { 24 | arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 25 | defer arena.deinit(); 26 | 27 | bout = std.io.bufferedWriter(std.io.getStdOut().writer()); 28 | defer bout.flush() catch {}; 29 | 30 | const argv = try std.process.argsAlloc(allo); 31 | defer std.process.argsFree(allo, argv); 32 | 33 | if (argv.len != 3) @panic("usage: livedecode "); 34 | 35 | // Compile script file, so we don't touch the binary 36 | // when the code isn't valid anyways. 37 | var script = ast.loadFile(argv[1]); 38 | 39 | var binfile = try std.fs.cwd().openFile(argv[2], .{}); 40 | defer binfile.close(); 41 | 42 | var state = State{ 43 | .script = script, 44 | .file = binfile, 45 | .fields = std.StringHashMap(Value).init(allo), 46 | .bitread = null, 47 | }; 48 | 49 | try state.fields.put("file.path", Value{ .str = argv[2] }); 50 | try state.fields.put("file.name", Value{ .str = std.fs.path.basename(argv[2]) }); 51 | try state.fields.put("file.size", Value{ .u64 = try state.file.getEndPos() }); 52 | 53 | try state.execute(script.top_level); 54 | } 55 | 56 | fn fetchType(state: *State, val_type: Type, length: usize) !Value { 57 | const reader = state.file.reader(); 58 | const value: Value = switch (val_type) { 59 | .u8 => Value{ .u8 = try reader.readInt(u8, state.endianess) }, 60 | .u16 => Value{ .u16 = try reader.readInt(u16, state.endianess) }, 61 | .u32 => Value{ .u32 = try reader.readInt(u32, state.endianess) }, 62 | .u64 => Value{ .u64 = try reader.readInt(u64, state.endianess) }, 63 | .i8 => Value{ .i8 = try reader.readInt(i8, state.endianess) }, 64 | .i16 => Value{ .i16 = try reader.readInt(i16, state.endianess) }, 65 | .i32 => Value{ .i32 = try reader.readInt(i32, state.endianess) }, 66 | .i64 => Value{ .i64 = try reader.readInt(i64, state.endianess) }, 67 | .f32 => Value{ .f32 = @bitCast(try reader.readInt(u32, state.endianess)) }, 68 | .f64 => Value{ .f64 = @bitCast(try reader.readInt(u64, state.endianess)) }, 69 | .str => blk: { 70 | var mem = try allo.alloc(u8, length); 71 | try reader.readNoEof(mem); 72 | break :blk Value{ .str = mem }; 73 | }, 74 | .blob => blk: { 75 | var mem = try allo.alloc(u8, length); 76 | try reader.readNoEof(mem); 77 | break :blk Value{ .blob = mem }; 78 | }, 79 | .bitblob => blk: { 80 | if (state.bitread) |*br| { 81 | var mem = try allo.alloc(u1, length); 82 | switch (state.endianess) { 83 | .Little => try readBits(&br.le, mem), 84 | .Big => try readBits(&br.be, mem), 85 | } 86 | break :blk Value{ .bitblob = mem }; 87 | } else { 88 | @panic("attempt to read bitblob in byteread mode"); 89 | } 90 | }, 91 | .bits => blk: { 92 | if (state.bitread) |*br| { 93 | if (length > 64) @panic("cannot read over 64 bits into an integer"); 94 | break :blk Value{ .bits = switch (state.endianess) { 95 | .Little => try br.le.readBitsNoEof(u64, length), 96 | .Big => try br.be.readBitsNoEof(u64, length), 97 | } }; 98 | } else { 99 | @panic("attempt to read bits in byteread mode"); 100 | } 101 | }, 102 | .tuple => unreachable, 103 | }; 104 | 105 | return value; 106 | } 107 | 108 | const Args = struct { 109 | state: *State, 110 | list: []const ast.ValueToken, 111 | index: usize, 112 | 113 | pub fn init(state: *State, list: []const ast.ValueToken) Args { 114 | return Args{ .state = state, .list = list, .index = 0 }; 115 | } 116 | 117 | pub fn hasMore(args: Args) bool { 118 | return (args.index < args.list.len); 119 | } 120 | 121 | pub fn next(args: *Args) ?ast.ValueToken { 122 | if (args.index >= args.list.len) 123 | return null; 124 | const current = args.list[args.index]; 125 | args.index += 1; 126 | return current; 127 | } 128 | 129 | pub fn getIdentifier(args: *Args) []const u8 { 130 | const tag = args.next() orelse @panic("not enough arguments!"); 131 | if (tag != .identifier) 132 | std.debug.panic("Expected identifier, got {s}!", .{@tagName(tag)}); 133 | return tag.identifier; 134 | } 135 | 136 | pub fn getValue(args: *Args) !Value { 137 | const tag = args.next() orelse @panic("not enough arguments!"); 138 | 139 | return try args.state.decode(tag); 140 | } 141 | 142 | pub fn getInt(args: *Args) u64 { 143 | const tag = args.next() orelse @panic("not enough arguments!"); 144 | return args.state.decodeInt(tag) catch @panic("expected integer"); 145 | } 146 | 147 | pub fn getString(args: *Args) []const u8 { 148 | const tag = args.next() orelse @panic("not enough arguments!"); 149 | 150 | var val = args.state.decode(tag) catch std.debug.panic("expected string value, got illegal {s}", .{@tagName(tag)}); 151 | 152 | return switch (val) { 153 | .str => |s| s, 154 | else => std.debug.panic("expected string value, got {s}", .{@tagName(val)}), 155 | }; 156 | } 157 | 158 | pub fn getTuple(args: *Args) []const Value { 159 | const tag = args.next() orelse @panic("not enough arguments, expected tuple."); 160 | if (tag != .tuple) std.debug.panic("expected tuple, got {s}", .{@tagName(tag)}); 161 | return (args.state.decode(tag) catch unreachable).tuple; 162 | } 163 | }; 164 | 165 | const Macros = struct { 166 | pub fn @"if"(state: *State, iter: *Args) !void { 167 | const condition = try state.decode(iter.next().?); 168 | const is_met = if (iter.next()) |eql_str| blk: { 169 | const value = try state.decode(eql_str); 170 | 171 | break :blk (condition.getInt() == value.getInt()); 172 | } else condition.getInt() != 0; 173 | try state.condstack.append(is_met); 174 | } 175 | 176 | // pub fn @"elseif"(state: *State, iter: *Args) !void { 177 | // try @"endif"(state, iter); 178 | // try @"if"(state, iter); 179 | // } 180 | 181 | pub fn @"else"(state: *State, iter: *Args) !void { 182 | _ = iter; 183 | const last = &state.condstack.items[state.condstack.items.len - 1]; 184 | last.* = !last.*; 185 | } 186 | 187 | pub fn endif(state: *State, iter: *Args) !void { 188 | _ = iter; 189 | _ = state.condstack.pop(); 190 | } 191 | 192 | // .loop 193 | // .loop 194 | pub fn loop(state: *State, iter: *Args) !void { 195 | var lop = State.Loop{ 196 | .start_offset = try state.code.getPos(), 197 | .count = 0, 198 | .limit = try state.decodeInt(iter.next().?), 199 | .loopvar = try allo.dupe(u8, iter.next() orelse ""), 200 | }; 201 | try state.repeatstack.append(lop); 202 | if (lop.loopvar.len > 0) { 203 | try state.fields.put(lop.loopvar, Value{ .u64 = lop.count }); 204 | } 205 | } 206 | 207 | pub fn endloop(state: *State, iter: *Args) !void { 208 | _ = iter; 209 | const lop = &state.repeatstack.items[state.repeatstack.items.len - 1]; 210 | 211 | if (lop.count == lop.limit) { 212 | _ = state.repeatstack.pop(); 213 | } else { 214 | lop.count += 1; 215 | if (lop.loopvar.len > 0) { 216 | try state.fields.put(lop.loopvar, Value{ .u64 = lop.count }); 217 | } 218 | try state.code.seekTo(lop.start_offset); 219 | } 220 | } 221 | }; 222 | 223 | const Commands = struct { 224 | pub fn def(state: *State, iter: *Args) !void { 225 | const name = iter.getIdentifier(); 226 | const value = try iter.getValue(); 227 | state.setVariable(name, value); 228 | } 229 | 230 | pub fn print(state: *State, iter: *Args) !void { 231 | var suppress_space = false; 232 | 233 | while (iter.next()) |item| { 234 | if (item == .identifier and std.mem.eql(u8, item.identifier, ";")) { 235 | suppress_space = true; 236 | } else if (item == .identifier) { 237 | write("{s}", .{item.identifier}); 238 | if (!suppress_space) { 239 | write(" ", .{}); 240 | } 241 | suppress_space = false; 242 | } else { 243 | var val = state.decode(item) catch |err| Value{ .str = @errorName(err) }; 244 | write("{}", .{val}); 245 | if (!suppress_space) { 246 | write(" ", .{}); 247 | } 248 | suppress_space = false; 249 | } 250 | } 251 | 252 | if (!suppress_space) { 253 | write("\n", .{}); 254 | } 255 | } 256 | 257 | pub fn seek(state: *State, iter: *Args) !void { 258 | if (state.bitread != null) @panic("cannot seek while reading bits"); 259 | var offset: u64 = 0; 260 | var good = false; 261 | while (iter.next()) |item| { 262 | good = true; 263 | offset += try state.decodeInt(item); 264 | } 265 | if (!good) 266 | return; 267 | try state.file.seekTo(offset); 268 | } 269 | 270 | pub fn move(state: *State, iter: *Args) !void { 271 | if (state.bitread != null) @panic("cannot move while reading bits"); 272 | var offset: i64 = 0; 273 | var good = false; 274 | while (iter.next()) |item| { 275 | good = true; 276 | offset += try state.decodeSignedInt(item); 277 | } 278 | if (!good) 279 | return; 280 | try state.file.seekBy(offset); 281 | } 282 | 283 | pub fn tell(state: *State, iter: *Args) !void { 284 | const where = try state.file.getPos(); 285 | if (iter.hasMore()) { 286 | state.setVariable(iter.getIdentifier(), .{ .u64 = where }); 287 | } 288 | write("current file position: {}\n", .{where}); 289 | } 290 | 291 | pub fn dump(state: *State, iter: *Args) !void { 292 | const len = try state.decodeInt(iter.next().?); 293 | 294 | var line_buffer: [16]u8 = undefined; 295 | 296 | var i: u64 = 0; 297 | var where = try state.file.getPos(); 298 | 299 | while (i < len) : (i += line_buffer.len) { 300 | const bytes = std.math.min(line_buffer.len, len - i); 301 | const actual = try state.file.read(line_buffer[0..bytes]); 302 | 303 | write("0x{X:0>8}:", .{where}); 304 | 305 | for (line_buffer[0..actual], 0..) |c, j| { 306 | const col = j; 307 | if (col % 4 == 0 and col > 0) write(" ", .{}); 308 | write(" {X:0>2}", .{c}); 309 | } 310 | for (line_buffer[actual..], 0..) |_, j| { 311 | const col = actual + j; 312 | if (col % 4 == 0 and col > 0) write(" ", .{}); 313 | write(" __", .{}); 314 | } 315 | 316 | write(" |", .{}); 317 | 318 | for (line_buffer[0..actual]) |c| { 319 | write("{c}", .{ 320 | if (std.ascii.isPrint(c)) c else '.', 321 | }); 322 | } 323 | for (line_buffer[actual..]) |_| { 324 | write(" ", .{}); 325 | } 326 | 327 | write("|\n", .{}); 328 | 329 | where += actual; 330 | if (actual < bytes) 331 | break; 332 | } 333 | } 334 | 335 | pub fn diskdump(state: *State, iter: *Args) !void { 336 | const len = iter.getInt(); 337 | const filename = iter.getString(); 338 | 339 | var out = std.fs.cwd().createFile(filename, .{}) catch @panic("i/o error"); 340 | defer out.close(); 341 | 342 | var i: u64 = 0; 343 | while (i < len) { 344 | var buffer: [8192]u8 = undefined; 345 | 346 | const bytes = try state.file.read(&buffer); 347 | if (bytes == 0) @panic("not enough data"); 348 | 349 | out.writer().writeAll(buffer[0..bytes]) catch @panic("i/o error"); 350 | } 351 | } 352 | 353 | // program 354 | pub fn call(state: *State, iter: *Args) !void { 355 | const pgm_name = iter.getIdentifier(); 356 | 357 | const program = for (state.script.programs) |prg| { 358 | if (std.mem.eql(u8, prg.name, pgm_name)) 359 | break prg; 360 | } else std.debug.panic("program {s} not found!", .{pgm_name}); 361 | 362 | var argc: usize = 0; 363 | while (iter.next()) |argv| : (argc += 1) { 364 | const name = try std.fmt.allocPrint(allo, "arg[{}]", .{argc}); 365 | try state.fields.put(name, try state.decode(argv)); 366 | } 367 | 368 | try state.execute(program.code); 369 | } 370 | 371 | pub fn endian(state: *State, iter: *Args) !void { 372 | if (state.bitread != null) { 373 | // there isn't really a logical way to do this since the endian 374 | // affects the order in which the bitreader goes through bytes 375 | @panic("cannot switch endianness while reading bits"); 376 | } 377 | 378 | const kind = iter.getIdentifier(); 379 | 380 | if (std.ascii.eqlIgnoreCase(kind, "little") or std.ascii.eqlIgnoreCase(kind, "le")) { 381 | state.endianess = .Little; 382 | } else if (std.ascii.eqlIgnoreCase(kind, "big") or std.ascii.eqlIgnoreCase(kind, "be")) { 383 | state.endianess = .Big; 384 | } else { 385 | std.debug.panic("invalid endianess: {s}", .{kind}); 386 | } 387 | } 388 | 389 | pub fn bitread(state: *State, iter: *Args) !void { 390 | _ = iter; 391 | state.bitread = switch (state.endianess) { 392 | .Little => .{ .le = std.io.bitReader(.Little, state.file.reader()) }, 393 | .Big => .{ .be = std.io.bitReader(.Big, state.file.reader()) }, 394 | }; 395 | } 396 | 397 | pub fn byteread(state: *State, iter: *Args) !void { 398 | _ = iter; 399 | state.bitread = null; 400 | } 401 | 402 | pub fn bitmap(state: *State, iter: *Args) !void { 403 | const PixelFormat = enum { 404 | rgb565, 405 | rgb888, 406 | bgr888, 407 | rgbx8888, 408 | rgba8888, 409 | pub fn bpp(fmt: @This()) usize { 410 | return switch (fmt) { 411 | .rgb565 => 2, 412 | .rgb888 => 3, 413 | .bgr888 => 3, 414 | .rgbx8888 => 4, 415 | .rgba8888 => 4, 416 | }; 417 | } 418 | }; 419 | 420 | const width = iter.getInt(); 421 | const height = iter.getInt(); 422 | const format = std.meta.stringToEnum(PixelFormat, iter.getIdentifier()) orelse @panic("invalid bitmap format"); 423 | 424 | const writeout = if (iter.next()) |tag| 425 | (try state.decode(tag)).str 426 | else 427 | ""; 428 | 429 | const buffer_size = width * height * format.bpp(); 430 | 431 | if (writeout.len > 0) { 432 | var out = std.fs.cwd().createFile(writeout, .{}) catch @panic("i/o error"); 433 | defer out.close(); 434 | 435 | out.writer().print("P6 {} {} 255\n", .{ width, height }) catch @panic("i/o error"); 436 | 437 | switch (format) { 438 | .rgb565 => { 439 | var i: usize = 0; 440 | while (i < buffer_size) { 441 | var copy: [2]u8 = undefined; 442 | state.file.reader().readNoEof(©) catch @panic("i/o error"); 443 | 444 | const Rgb = packed struct { 445 | r: u5, 446 | g: u6, 447 | b: u5, 448 | }; 449 | 450 | const rgb: Rgb = @bitCast(copy); 451 | 452 | var vals = [3]u8{ 453 | @as(u8, rgb.b) << 3 | @as(u8, rgb.b) >> 2, 454 | @as(u8, rgb.g) << 2 | @as(u8, rgb.g) >> 4, 455 | @as(u8, rgb.r) << 3 | @as(u8, rgb.r) >> 2, 456 | }; 457 | 458 | out.writeAll(&vals) catch @panic("i/o error"); 459 | 460 | i += copy.len; 461 | } 462 | }, 463 | .rgb888 => { 464 | var i: usize = 0; 465 | while (i < buffer_size) { 466 | var copy: [8192]u8 = undefined; 467 | const maxlen = std.math.min(copy.len, buffer_size - i); 468 | 469 | const len = state.file.read(copy[0..maxlen]) catch @panic("i/o error"); 470 | if (len == 0) @panic("unexpected eof in bitmap"); 471 | 472 | out.writeAll(copy[0..len]) catch @panic("i/o error"); 473 | 474 | i += copy.len; 475 | } 476 | }, 477 | .bgr888 => { 478 | var i: usize = 0; 479 | while (i < buffer_size) { 480 | var copy: [3]u8 = undefined; 481 | state.file.reader().readNoEof(©) catch @panic("i/o error"); 482 | 483 | std.mem.swap(u8, ©[0], ©[2]); 484 | 485 | out.writeAll(©) catch @panic("i/o error"); 486 | 487 | i += copy.len; 488 | } 489 | }, 490 | .rgbx8888 => @panic("rgbx8888 not supported for writeout yet."), 491 | .rgba8888 => @panic("rgba8888 not supported for writeout yet."), 492 | } 493 | } else { 494 | try state.file.seekBy(@intCast(buffer_size)); 495 | } 496 | } 497 | 498 | pub fn lut(state: *State, iter: *Args) !void { 499 | const value = try state.decodeInt(iter.next().?); 500 | 501 | // TODO: Port to tuples 502 | while (iter.next()) |kv_src| { 503 | if (kv_src != .tuple) @panic("lut expects a list of tuples!"); 504 | 505 | const kv = (state.decode(kv_src) catch unreachable).tuple; 506 | 507 | if (kv.len != 2) @panic("lut expects 2-tuples."); 508 | 509 | const key = kv[0].getInt(); 510 | const tag = kv[1].str; 511 | 512 | if (value == key) { 513 | write("{} => {s}\n", .{ key, tag }); 514 | } 515 | } 516 | } 517 | 518 | pub fn divs(state: *State, iter: *Args) !void { 519 | const value = try state.decodeInt(iter.next().?); 520 | 521 | write("{} is divided by ", .{value}); 522 | 523 | var i: u64 = value; 524 | var first = true; 525 | while (i > 0) : (i -= 1) { 526 | if ((value % i) == 0) { 527 | if (!first) write(", ", .{}); 528 | write("{}", .{i}); 529 | first = false; 530 | } 531 | } 532 | write("\n", .{}); 533 | } 534 | 535 | pub fn findpattern(state: *State, iter: *Args) !void { 536 | const MatchSet = std.bit_set.ArrayBitSet(usize, 256); 537 | 538 | var list = std.ArrayList(MatchSet).init(allo); 539 | defer list.deinit(); 540 | 541 | while (iter.next()) |item| { 542 | if (item == .identifier and std.mem.eql(u8, item.identifier, "*")) { 543 | try list.append(MatchSet.initFull()); 544 | } else { 545 | var set = MatchSet.initEmpty(); 546 | var opts = std.mem.tokenize(u8, item.identifier, "|"); 547 | while (opts.next()) |key| { 548 | const index = try std.fmt.parseInt(u8, key, 0); 549 | set.set(index); 550 | } 551 | try list.append(set); 552 | } 553 | } 554 | 555 | var buffer = try allo.alloc(u8, list.items.len); 556 | defer allo.free(buffer); 557 | 558 | const H = struct { 559 | fn isMatch(pattern: []const MatchSet, seq: []const u8) bool { 560 | std.debug.assert(pattern.len == seq.len); 561 | for (pattern, 0..) |mc, i| { 562 | if (!mc.isSet(seq[i])) 563 | return false; 564 | } 565 | return true; 566 | } 567 | }; 568 | 569 | try state.file.reader().readNoEof(buffer); 570 | 571 | while (true) { 572 | if (H.isMatch(list.items, buffer)) { 573 | const offset = try state.file.getPos(); 574 | write("found match at {}: {any}\n", .{ offset - buffer.len, buffer }); 575 | } 576 | 577 | std.mem.copy(u8, buffer[0..], buffer[1..]); 578 | buffer[buffer.len - 1] = try state.file.reader().readByte(); 579 | } 580 | } 581 | 582 | // array ? 583 | pub fn array(state: *State, iter: *Args) !void { 584 | const type_name = iter.getIdentifier(); 585 | const val_type = std.meta.stringToEnum(Type, type_name) orelse std.debug.panic("unknown type: {s}", .{type_name}); 586 | const val_length = if (val_type.requiresLength()) 587 | try state.decodeInt(iter.next().?) 588 | else 589 | 0; 590 | 591 | const name_pattern = iter.getIdentifier(); 592 | const name_pattern_split = std.mem.indexOfScalar(u8, name_pattern, '?') orelse @panic("name requires ? placeholder for index"); 593 | 594 | const length = try state.decodeInt(iter.next().?); 595 | 596 | var i: usize = 0; 597 | while (i < length) : (i += 1) { 598 | var index_buf: [32]u8 = undefined; 599 | const index = std.fmt.bufPrint(&index_buf, "{d}", .{i}) catch unreachable; 600 | 601 | const value = try fetchType(state, val_type, val_length); 602 | 603 | const name = try std.mem.join(allo, "", &.{ 604 | name_pattern[0..name_pattern_split], 605 | index, 606 | name_pattern[name_pattern_split + 1 ..], 607 | }); 608 | 609 | write("{s: >20} = {}\n", .{ name, value }); 610 | try state.fields.put(name, value); 611 | } 612 | } 613 | 614 | // template 615 | pub fn select(state: *State, iter: *Args) !void { 616 | const target_var = iter.getIdentifier(); 617 | const pattern = iter.getIdentifier(); 618 | 619 | var src_name = std.ArrayList(u8).init(allo); 620 | 621 | var pos: usize = 0; 622 | while (std.mem.indexOfScalarPos(u8, pattern, pos, '?')) |item| { 623 | defer pos = item + 1; 624 | 625 | const slice = pattern[pos..item]; 626 | try src_name.appendSlice(slice); 627 | 628 | const value = try state.decode(iter.next().?); 629 | 630 | try src_name.writer().print("{}", .{value}); 631 | } 632 | 633 | try src_name.appendSlice(pattern[pos..]); 634 | 635 | const value = state.fields.get(src_name.items) orelse std.debug.panic("Variable {s} not found!", .{src_name.items}); 636 | try state.fields.put(try allo.dupe(u8, target_var), value); 637 | } 638 | }; 639 | 640 | const State = struct { 641 | // io: 642 | file: std.fs.File, 643 | endianess: std.builtin.Endian = .Little, 644 | bitread: ?union { // non-null while in bitread mode 645 | le: std.io.BitReader(.Little, std.fs.File.Reader), 646 | be: std.io.BitReader(.Big, std.fs.File.Reader), 647 | }, 648 | 649 | // runtime: 650 | script: ast.Script, 651 | fields: std.StringHashMap(Value), 652 | 653 | fn decodeInt(state: State, tag: ast.ValueToken) !u64 { 654 | var val = try state.decode(tag); 655 | return val.getInt(); 656 | } 657 | 658 | fn decodeSignedInt(state: State, tag: ast.ValueToken) !i64 { 659 | var val = try state.decode(tag); 660 | return val.getSignedInt(); 661 | } 662 | 663 | fn decode(state: State, tag: ast.ValueToken) !Value { 664 | return switch (tag) { 665 | .variable_ref => |name| state.fields.get(name) orelse std.debug.panic("var {s} not found", .{name}), 666 | .string => |str| Value{ .str = str }, 667 | .identifier => |id| Value{ .str = id }, 668 | .number => |str| if (std.fmt.parseInt(i64, str, 0)) |sval| 669 | Value{ .i64 = sval } 670 | else |_| if (std.fmt.parseInt(u64, str, 0)) |ival| 671 | Value{ .u64 = ival } 672 | else |_| 673 | Value{ .f32 = try std.fmt.parseFloat(f32, str) }, 674 | .tuple => |src| blk: { 675 | const tup = try allo.alloc(Value, src.len); 676 | for (tup, 0..) |*dst, i| { 677 | dst.* = try state.decode(src[i]); 678 | } 679 | break :blk Value{ .tuple = tup }; 680 | }, 681 | }; 682 | } 683 | 684 | pub fn read(state: *State, buf: []u8) !usize { 685 | return if (state.bitread) |*br| switch (state.endianess) { 686 | .Little => br.le.reader().read(buf), 687 | .Big => br.be.reader().read(buf), 688 | } else state.file.reader().read(buf); 689 | } 690 | 691 | fn reader(state: *State) std.io.Reader(*State, std.fs.File.Reader.Error, read) { 692 | return .{ .context = state }; 693 | } 694 | 695 | pub fn setVariable(state: *State, name: []const u8, value: Value) void { 696 | const gop = state.fields.getOrPut(name) catch @panic("oom"); 697 | if (!gop.found_existing) { 698 | gop.key_ptr.* = allo.dupe(u8, name) catch @panic("oom"); 699 | } 700 | gop.value_ptr.* = value; 701 | } 702 | 703 | const ExecError = error{ OutOfMemory, InvalidCharacter, Overflow, EndOfStream } || std.fs.File.ReadError || std.fs.File.SeekError; 704 | pub fn execute(state: *State, seq: ast.Sequence) ExecError!void { 705 | for (seq.instructions) |instr| { 706 | try state.executeOne(instr); 707 | } 708 | } 709 | 710 | pub fn executeOne(state: *State, instr: ast.Node) ExecError!void { 711 | switch (instr) { 712 | .command => |info| { 713 | var cmditer = Args.init(state, info.arguments); 714 | 715 | inline for (@typeInfo(Commands).Struct.decls) |decl| { 716 | if (std.mem.eql(u8, decl.name, info.name)) { 717 | try @field(Commands, decl.name)(state, &cmditer); 718 | return; 719 | } 720 | } 721 | 722 | // type name 723 | const val_type = std.meta.stringToEnum(Type, info.name) orelse std.debug.panic("unknown type: {s}", .{info.name}); 724 | 725 | const length = if (val_type.requiresLength()) 726 | try state.decodeInt(cmditer.next().?) 727 | else 728 | 0; 729 | 730 | const value = try fetchType(state, val_type, length); 731 | 732 | if (cmditer.next()) |name| { 733 | write("{s: >20} = {}\n", .{ name.identifier, value }); 734 | state.setVariable(name.identifier, value); 735 | } 736 | }, 737 | .decision => |info| { 738 | const value = try state.decodeInt(info.value); 739 | 740 | const equals = if (info.comparison) |comp| 741 | value == try state.decodeInt(comp) 742 | else 743 | value != 0; 744 | 745 | try state.execute(if (equals) 746 | info.true_body 747 | else 748 | info.false_body orelse return); 749 | }, 750 | .loop => |info| { 751 | const limit = try state.decodeInt(info.count); 752 | 753 | var count: u64 = 0; 754 | while (count < limit) : (count += 1) { 755 | if (info.variable) |varname| 756 | state.setVariable(varname, Value{ .u64 = count }); 757 | try state.execute(info.body); 758 | } 759 | }, 760 | .breakloop => @panic("not implemented yet"), 761 | } 762 | } 763 | }; 764 | 765 | const Value = union(Type) { 766 | u8: u8, 767 | u16: u16, 768 | u32: u32, 769 | u64: u64, 770 | i8: i8, 771 | i16: i16, 772 | i32: i32, 773 | i64: i64, 774 | f32: f32, 775 | f64: f64, 776 | str: []const u8, 777 | blob: []const u8, 778 | tuple: []Value, 779 | 780 | // bitread cases 781 | bitblob: []const u1, // this is really inefficient but it's fine 782 | bits: u64, 783 | 784 | pub fn getInt(val: Value) u64 { 785 | return switch (val) { 786 | .u8 => |v| v, 787 | .u16 => |v| v, 788 | .u32 => |v| v, 789 | .u64 => |v| v, 790 | .i8 => |v| @bitCast(v), 791 | .i16 => |v| @bitCast(v), 792 | .i32 => |v| @bitCast(v), 793 | .i64 => |v| @bitCast(v), 794 | .f32 => |v| @intFromFloat(v), 795 | .f64 => |v| @intFromFloat(v), 796 | .str => |v| std.fmt.parseInt(u64, v, 0) catch unreachable, 797 | .blob => @panic("blob is not an int"), 798 | .bitblob => @panic("bitblob is not an int"), 799 | .bits => |v| v, 800 | .tuple => @panic("tuple is not convertible to int."), 801 | }; 802 | } 803 | 804 | pub fn getSignedInt(val: Value) i64 { 805 | return switch (val) { 806 | .u8 => |v| @bitCast(v), 807 | .u16 => |v| @bitCast(v), 808 | .u32 => |v| @bitCast(v), 809 | .u64 => |v| @bitCast(v), 810 | .i8 => |v| v, 811 | .i16 => |v| v, 812 | .i32 => |v| v, 813 | .i64 => |v| v, 814 | .f32 => |v| @intFromFloat(v), 815 | .f64 => |v| @intFromFloat(v), 816 | .str => |v| std.fmt.parseInt(i64, v, 0) catch unreachable, 817 | .blob => @panic("blob is not an int"), 818 | .bitblob => @panic("bitblob is not an int"), 819 | .bits => |v| @intCast(v), // TODO we might want to store the number of bits used so we can convert to signed 820 | .tuple => @panic("tuple is not convertible to int."), 821 | }; 822 | } 823 | 824 | pub fn format(val: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 825 | _ = fmt; 826 | _ = options; 827 | switch (val) { 828 | .str => |str| try writer.print("\"{}\"", .{std.zig.fmtEscapes(str)}), 829 | .blob => |str| try writer.print("{any}", .{str}), 830 | .tuple => |tup| { 831 | try writer.writeAll("("); 832 | for (tup, 0..) |item, i| { 833 | if (i > 0) 834 | try writer.writeAll(" "); 835 | try writer.print("{}", .{item}); 836 | } 837 | try writer.writeAll(")"); 838 | }, 839 | inline else => |v| try writer.print("{d}", .{v}), 840 | } 841 | } 842 | }; 843 | 844 | fn readBits(br: anytype, out: []u1) !void { 845 | for (out) |*b| { 846 | b.* = try br.readBitsNoEof(u1, 1); 847 | } 848 | } 849 | 850 | const Type = enum { 851 | u8, 852 | u16, 853 | u32, 854 | u64, 855 | i8, 856 | i16, 857 | i32, 858 | i64, 859 | f32, 860 | f64, 861 | str, 862 | blob, 863 | bitblob, 864 | bits, 865 | tuple, 866 | 867 | pub fn requiresLength(t: Type) bool { 868 | return switch (t) { 869 | .u8 => false, 870 | .u16 => false, 871 | .u32 => false, 872 | .u64 => false, 873 | .i8 => false, 874 | .i16 => false, 875 | .i32 => false, 876 | .i64 => false, 877 | .f32 => false, 878 | .f64 => false, 879 | .str => true, 880 | .blob => true, 881 | .bitblob => true, 882 | .bits => true, 883 | .tuple => @panic("tuples cannot be read from the stream"), 884 | }; 885 | } 886 | }; 887 | --------------------------------------------------------------------------------