reader/reader.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/io/filepath"; | ||
| 5 | import "std/io/file"; | ||
| 6 | |||
| 7 | // Own imports | ||
| 8 | import "@/exception/compiler-error"; | ||
| 9 | import "@/exception/fatal-error"; | ||
| 10 | import "@/reader/code-loc"; | ||
| 11 | import "@/util/test-util"; | ||
| 12 | |||
| 13 | 3953 | public type Reader struct { | |
| 14 | File file | ||
| 15 | FilePath filePath | ||
| 16 | 3966 | char curChar = '\0' | |
| 17 | CodeLoc curCodeLoc | ||
| 18 | String sourceCode // All chars, that were read so far | ||
| 19 | 3966 | unsigned long curOffset = 0l // Offset of the current char within the source code | |
| 20 | // One char lookahead buffer, filled by peekChar() | ||
| 21 | 3966 | char peekedChar = '\0' | |
| 22 | 3966 | bool hasPeekedChar = false | |
| 23 | } | ||
| 24 | |||
| 25 | 3966 | public p Reader.ctor(const FilePath& inputPath) { | |
| 26 | 3966 | this.filePath = inputPath; | |
| 27 | 3966 | this.curCodeLoc = CodeLoc(1l, 0l, inputPath.toString()); | |
| 28 | |||
| 29 | // Open the file input stream | ||
| 30 | 3966 | Result<File> result = openFile(inputPath.toString(), MODE_READ); | |
| 31 | 3966 | if !result.isOk() { | |
| 32 | ✗ | const String messageSuffix = "Source file at path '" + inputPath.toString() + "' cannot be opened."; | |
| 33 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, messageSuffix.getRaw()); | |
| 34 | ✗ | abortWithError(error.getMessage()); | |
| 35 | ✗ | } | |
| 36 | 3966 | this.file = result.unwrap(); | |
| 37 | |||
| 38 | // Read the first character | ||
| 39 | 3966 | this.advance(); | |
| 40 | 3966 | } | |
| 41 | |||
| 42 | /** | ||
| 43 | * Get the previously read character | ||
| 44 | * | ||
| 45 | * @return char Last character | ||
| 46 | */ | ||
| 47 | 41921995 | public inline f<char> Reader.getChar() { | |
| 48 | 41921995 | return this.curChar; | |
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Get the code location of the previously read character | ||
| 53 | * | ||
| 54 | * @return CodeLoc Code location | ||
| 55 | */ | ||
| 56 | 2663888 | public inline f<CodeLoc&> Reader.getCodeLoc() { | |
| 57 | 2663888 | return this.curCodeLoc; | |
| 58 | } | ||
| 59 | |||
| 60 | /** | ||
| 61 | * Peek the character after the previously read character without consuming it | ||
| 62 | * | ||
| 63 | * @return char Next character | ||
| 64 | */ | ||
| 65 | 154635 | public f<char> Reader.peekChar() { | |
| 66 | 154635 | if !this.hasPeekedChar { | |
| 67 | 154635 | if this.file.isEOF() { | |
| 68 | ✗ | return '\0'; | |
| 69 | } | ||
| 70 | 154635 | this.peekedChar = this.file.readChar(); | |
| 71 | 154635 | this.hasPeekedChar = true; | |
| 72 | 154635 | } | |
| 73 | 154635 | return this.peekedChar; | |
| 74 | } | ||
| 75 | |||
| 76 | /** | ||
| 77 | * Advance the reader by one character | ||
| 78 | */ | ||
| 79 | 19974810 | public p Reader.advance() { | |
| 80 | 39949620 | assert !this.isEOF(); | |
| 81 | 19974810 | if this.hasPeekedChar { | |
| 82 | 154635 | this.curChar = this.peekedChar; | |
| 83 | 154635 | this.hasPeekedChar = false; | |
| 84 | 154635 | } else { | |
| 85 | 19820175 | this.curChar = this.file.readChar(); | |
| 86 | 19820175 | } | |
| 87 | // Keep track of the source code, so that parts of it can be retrieved later on | ||
| 88 | 19974810 | this.curOffset = this.sourceCode.getLength(); | |
| 89 | 39949620 | if !this.isEOF() { | |
| 90 | 19970845 | this.sourceCode += this.curChar; | |
| 91 | 19970845 | } | |
| 92 | 19974810 | if this.curChar == '\n' { | |
| 93 | 672984 | this.curCodeLoc.line++; | |
| 94 | 672984 | this.curCodeLoc.col = 0l; | |
| 95 | 39276636 | } else if !this.isUtf8ContinuationByte(this.curChar) { | |
| 96 | // Count code points instead of bytes | ||
| 97 | 19301510 | this.curCodeLoc.col++; | |
| 98 | 19301826 | } | |
| 99 | 19974810 | } | |
| 100 | |||
| 101 | /** | ||
| 102 | * Check if the given char is a continuation byte of a multibyte UTF-8 sequence (0b10xxxxxx) | ||
| 103 | * | ||
| 104 | * @param c Char to check | ||
| 105 | * @return Continuation byte or not | ||
| 106 | */ | ||
| 107 | inline f<bool> Reader.isUtf8ContinuationByte(char c) { | ||
| 108 | const int byteValue = cast<int>(cast<byte>(c)); | ||
| 109 | return (byteValue & 0xC0) == 0x80; | ||
| 110 | } | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Get the offset of the current char within the source code | ||
| 114 | * | ||
| 115 | * @return Offset in bytes | ||
| 116 | */ | ||
| 117 | 5327775 | public inline f<unsigned long> Reader.getOffset() { | |
| 118 | 5327775 | return this.curOffset; | |
| 119 | } | ||
| 120 | |||
| 121 | /** | ||
| 122 | * Get a part of the source code, that was read so far | ||
| 123 | * | ||
| 124 | * @param startOffset Offset of the first char | ||
| 125 | * @param endOffset Offset after the last char | ||
| 126 | * @return Source code snippet | ||
| 127 | */ | ||
| 128 | 4081 | public f<String> Reader.getSourceCode(unsigned long startOffset, unsigned long endOffset) { | |
| 129 | 4081 | assert startOffset <= endOffset; | |
| 130 | 4081 | return this.sourceCode.getSubstring(startOffset, cast<long>(endOffset - startOffset)); | |
| 131 | } | ||
| 132 | |||
| 133 | /** | ||
| 134 | * Advance the reader by one character and check if this char equals the expected. | ||
| 135 | * | ||
| 136 | * @param c Expected char | ||
| 137 | */ | ||
| 138 | 150593 | public inline p Reader.expect(char c) { | |
| 139 | 150593 | assert this.curChar == c; | |
| 140 | 150593 | this.advance(); | |
| 141 | 150593 | } | |
| 142 | |||
| 143 | /** | ||
| 144 | * Check if we are at the end of the input file | ||
| 145 | * | ||
| 146 | * @return At the end or not | ||
| 147 | */ | ||
| 148 | 14034817 | public inline f<bool> Reader.isEOF() { | |
| 149 | // If a char was peeked, the current char is no EOF, even if the file reached its end while peeking | ||
| 150 | 14034817 | if this.hasPeekedChar { | |
| 151 | 1341 | return false; | |
| 152 | } | ||
| 153 | 14033476 | return this.file.isEOF(); | |
| 154 | } | ||
| 155 | // ─── tests ──────────────────────────────────────────────────────────────────── | ||
| 156 | |||
| 157 | 1 | #[test] | |
| 158 | f<bool> testReaderReadsFileContent() { | ||
| 159 | 1 | const FilePath testDir = makeUniqueTestDir("spice-reader-test"); | |
| 160 | 1 | const FilePath testFilePath = testDir / "file.txt"; | |
| 161 | 1 | const string expectedContent = "This is some test content\nLine 2\n\nLine 4"; | |
| 162 | 1 | writeTestFile(testFilePath, expectedContent); | |
| 163 | |||
| 164 | // Read the whole file | ||
| 165 | 1 | Reader reader = Reader(testFilePath); | |
| 166 | 42 | while !reader.isEOF() { | |
| 167 | 40 | reader.advance(); | |
| 168 | 40 | } | |
| 169 | 1 | const String content = reader.getSourceCode(0l, getRawLength(expectedContent)); | |
| 170 | 1 | const CodeLoc& endCodeLoc = reader.getCodeLoc(); | |
| 171 | 1 | const unsigned long endLine = endCodeLoc.line; | |
| 172 | |||
| 173 | // Peek a char without consuming it | ||
| 174 | 1 | Reader peekingReader = Reader(testFilePath); | |
| 175 | 1 | const char firstChar = peekingReader.getChar(); | |
| 176 | 1 | const char peekedChar = peekingReader.peekChar(); | |
| 177 | 1 | const char charAfterPeek = peekingReader.getChar(); | |
| 178 | 1 | peekingReader.advance(); | |
| 179 | 1 | const char secondChar = peekingReader.getChar(); | |
| 180 | 1 | const CodeLoc& secondCodeLoc = peekingReader.getCodeLoc(); | |
| 181 | 1 | const unsigned long secondLine = secondCodeLoc.line; | |
| 182 | 1 | const unsigned long secondCol = secondCodeLoc.col; | |
| 183 | |||
| 184 | // Clean up before asserting, so that a failure does not leave the file behind | ||
| 185 | 1 | removeTestDir(testDir); | |
| 186 | 1 | assert content == expectedContent; | |
| 187 | 1 | assert endLine == 4l; | |
| 188 | 1 | assert firstChar == 'T'; | |
| 189 | 1 | assert peekedChar == 'h'; | |
| 190 | 1 | assert charAfterPeek == 'T'; | |
| 191 | 1 | assert secondChar == 'h'; | |
| 192 | 1 | assert secondLine == 1l; | |
| 193 | 1 | assert secondCol == 2l; | |
| 194 | 1 | return true; | |
| 195 | 1 | } | |
| 196 |