GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
88 / 93
94.6%
Branches:
19 / 24, 24 excl
79.2%
Functions:
11 / 12
91.7%

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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3966 times.
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
1/2
✓ Branch 0 taken 3966 times.
✗ Branch 1 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 154635 times.
154635 if !this.hasPeekedChar {
67
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 154635 times.
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
2/2
✓ Branch 0 taken 154635 times.
✓ Branch 1 taken 19820175 times.
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
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 19974810 times.
✓ Branch 2 taken 3965 times.
✓ Branch 3 taken 19970845 times.
39949620 if !this.isEOF() {
90 19970845 this.sourceCode += this.curChar;
91 19970845 }
92
2/2
✓ Branch 0 taken 672984 times.
✓ Branch 1 taken 19301826 times.
19974810 if this.curChar == '\n' {
93 672984 this.curCodeLoc.line++;
94 672984 this.curCodeLoc.col = 0l;
95
4/4
✓ Branch 0 taken 316 times.
✓ Branch 1 taken 19301510 times.
✓ Branch 2 taken 316 times.
✓ Branch 3 taken 19301510 times.
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
2/2
✓ Branch 0 taken 1341 times.
✓ Branch 1 taken 14033476 times.
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
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 1 time.
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