Skip to content

Commit 3f61316

Browse files
authored
Merge pull request #101 from zaskar9/scanner-review
Code Review of Scanner Implementation ### Bug Fixes - `scanString()`: added EOF guard to prevent infinite loop on unterminated string literals - `scanString()`: replaced `boost::replace_all`-based `unescape()` with a character-by-character loop, fixing incorrect handling of sequences such as `\\n` and silent deletion of `\0` - `escape()`: same rewrite to fix ordering bug where introduced backslashes were doubled - `scanComment()`: added missing EOF guard in innermost `while (ch_ == '(')` loop - `scanNumber()`: added `charNo_--` after `seekg` to fix source-position tracking for `..` ### Error Handling - Replaced all `exit(1)` calls in the scanner with `throw ScannerError()` - Added `ScannerError` tag-type exception (`struct ScannerError : std::exception`) to `Scanner.h` - Added `catch (const ScannerError &)` at the top level in `olang/main.cpp` and `utils/grammar/main.cpp` ### API Improvements - Split `peek(bool advance = false)` into `peek()` and `peekAhead()` for clarity - Fixed `seek()`: added `file_.clear()`, reset `eof_`, and replaced `ch_ = '\0'` with `read()`; made private - Changed `path_` from `const path &` (dangling reference risk) to a value type; constructor now takes by value and moves ### Code Quality - Removed namespace-level `using` directives from `Scanner.h`; moved required ones to `Scanner.cpp` - Added `static_cast<unsigned char>` to all `<cctype>` call sites to eliminate undefined behaviour on non-ASCII input - Dropped `TokenType : char` underlying type in favour of the default `int` - Changed `EMPTY_POS`, `EMPTY_LOC`, and `to_string` templates in `global.h` from `static` to `inline` - Fixed missing `const` on `UndefinedToken::value()` ### Documentation - Added Doxygen-compatible doc comments to all public members of `Scanner.h` ### Tests - `test/unittests/scanner/UnterminatedString.Mod`: scanner terminates with an error on an unterminated string literal - `test/unittests/scanner/StringEscape.Mod`: `"\\n"` produces a two-character string, not a newline - `test/unittests/scanner/StringNullByte.Mod`: `\0` inside a string literal produces an embedded null byte
2 parents 578dc66 + 6b9034b commit 3f61316

12 files changed

Lines changed: 250 additions & 84 deletions

File tree

lib/include/global.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ struct SourceLoc {
3232
SourcePos start, end;
3333
};
3434

35-
static const FilePos EMPTY_POS = {"", 0, 0, 0 };
36-
static const SourceLoc EMPTY_LOC = {"", {0, 0, 0}, {0, 0, 0}};
35+
inline const FilePos EMPTY_POS = {"", 0, 0, 0 };
36+
inline const SourceLoc EMPTY_LOC = {"", {0, 0, 0}, {0, 0, 0}};
3737

3838
template <typename T>
39-
static std::string to_string(T obj) {
39+
inline std::string to_string(T obj) {
4040
std::stringstream stream;
4141
stream << obj;
4242
return stream.str();
4343
}
4444

4545
template <typename T>
46-
static std::string to_string(T *obj) {
46+
inline std::string to_string(T *obj) {
4747
std::stringstream stream;
4848
stream << *obj;
4949
return stream.str();

lib/scanner/Scanner.cpp

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,34 @@
1111
#include <memory>
1212
#include <sstream>
1313
#include <string>
14+
#include <utility>
1415

15-
#include <boost/algorithm/string.hpp>
16-
#include <boost/algorithm/string/replace.hpp>
1716
#include <boost/convert.hpp>
18-
#include <boost/convert/stream.hpp>
1917
#include <boost/lexical_cast.hpp>
18+
#include <boost/algorithm/string.hpp>
19+
#include <boost/convert/stream.hpp>
2020

2121
#include "IdentToken.h"
2222
#include "LiteralToken.h"
2323
#include "UndefinedToken.h"
2424

25+
using std::filesystem::path;
2526
using std::make_unique;
27+
using std::queue;
28+
using std::streampos;
2629
using std::string;
2730
using std::stringstream;
2831
using std::unique_ptr;
2932

30-
Scanner::Scanner(Logger &logger, const path &path) :
31-
logger_(logger), path_(path), lineNo_(1), charNo_(0), ch_{}, eof_(false) {
33+
ScannerError::~ScannerError() = default;
34+
35+
Scanner::Scanner(Logger &logger, path path) :
36+
logger_(logger), path_(std::move(path)), lineNo_(1), charNo_(0), ch_{}, eof_(false) {
3237
init();
3338
file_.open(path_.string(), std::ifstream::binary);
3439
if (!file_.is_open()) {
3540
logger_.error(string(), "cannot open file: " + path_.string() + ".");
36-
exit(1);
41+
throw ScannerError();
3742
}
3843
read();
3944
}
@@ -66,17 +71,20 @@ void Scanner::init() {
6671
{ "TRUE", TokenType::boolean_literal}, { "FALSE", TokenType::boolean_literal } };
6772
}
6873

69-
const Token* Scanner::peek(const bool advance) {
74+
const Token* Scanner::peek() {
7075
if (tokens_.empty()) {
7176
tokens_.push(scanToken());
72-
return tokens_.back().get();
7377
}
74-
if (advance) {
75-
const auto token = tokens_.back().get();
78+
return tokens_.front().get();
79+
}
80+
81+
const Token* Scanner::peekAhead() {
82+
if (tokens_.empty()) {
7683
tokens_.push(scanToken());
77-
return token;
7884
}
79-
return tokens_.front().get();
85+
const auto token = tokens_.back().get();
86+
tokens_.push(scanToken());
87+
return token;
8088
}
8189

8290
unique_ptr<const Token> Scanner::next() {
@@ -89,25 +97,27 @@ unique_ptr<const Token> Scanner::next() {
8997
}
9098

9199
void Scanner::seek(const FilePos &pos) {
100+
file_.clear();
92101
file_.seekg(pos.offset - static_cast<streampos>(1));
93102
queue<unique_ptr<const Token>> empty;
94103
std::swap(tokens_, empty);
95104
lineNo_ = pos.lineNo;
96105
charNo_ = pos.charNo - 1;
97-
ch_ = '\0';
106+
eof_ = false;
107+
read();
98108
}
99109

100110
unique_ptr<const Token> Scanner::scanToken() {
101111
// skip whitespace
102-
while (!eof_ && std::isspace(ch_)) {
112+
while (!eof_ && std::isspace(static_cast<unsigned char>(ch_))) {
103113
read();
104114
}
105115
FilePos pos = current();
106116
if (!eof_) {
107-
if (std::isalpha(ch_) || ch_ == '_') {
117+
if (std::isalpha(static_cast<unsigned char>(ch_)) || ch_ == '_') {
108118
return scanIdent();
109119
}
110-
if (std::isdigit(ch_)) {
120+
if (std::isdigit(static_cast<unsigned char>(ch_))) {
111121
return scanNumber();
112122
}
113123
if (ch_ == '"') {
@@ -168,7 +178,6 @@ unique_ptr<const Token> Scanner::scanToken() {
168178
case '.':
169179
read();
170180
if (!eof_ && ch_ == '.') {
171-
FilePos nextPos = current();
172181
read();
173182
if (!eof_ && ch_ == '.') {
174183
read();
@@ -230,7 +239,7 @@ void Scanner::read() {
230239
eof_ = true;
231240
} else {
232241
logger_.error(path_.string(), "error reading file.");
233-
exit(1);
242+
throw ScannerError();
234243
}
235244

236245
}
@@ -249,10 +258,14 @@ void Scanner::scanComment(const FilePos &pos) {
249258
while (true) {
250259
while (true) {
251260
while (ch_ == '(') {
252-
auto npos = current();
261+
auto nextPos = current();
253262
read();
263+
if (eof_) {
264+
logger_.error(pos, "comment not closed.");
265+
throw ScannerError();
266+
}
254267
if (ch_ == '*') {
255-
scanComment(npos);
268+
scanComment(nextPos);
256269
}
257270
}
258271
if (ch_ == '*') {
@@ -261,7 +274,7 @@ void Scanner::scanComment(const FilePos &pos) {
261274
}
262275
if (eof_) {
263276
logger_.error(pos, "comment not closed.");
264-
exit(1);
277+
throw ScannerError();
265278
}
266279
read();
267280
}
@@ -271,7 +284,7 @@ void Scanner::scanComment(const FilePos &pos) {
271284
}
272285
if (eof_) {
273286
logger_.error(pos, "comment not closed.");
274-
exit(1);
287+
throw ScannerError();
275288
}
276289
}
277290
}
@@ -282,7 +295,7 @@ unique_ptr<const Token> Scanner::scanIdent() {
282295
do {
283296
ss << ch_;
284297
read();
285-
} while (!eof_ && (std::isalnum(ch_) || ch_ == '_'));
298+
} while (!eof_ && (std::isalnum(static_cast<unsigned char>(ch_)) || ch_ == '_'));
286299
std::string ident = ss.str();
287300
if (const auto it = keywords_.find(ident); it != keywords_.end()) {
288301
if (it->second == TokenType::boolean_literal) {
@@ -299,14 +312,16 @@ unique_ptr<const Token> Scanner::scanNumber() {
299312
bool isChar = false;
300313
FilePos pos = current();
301314
std::stringstream ss;
302-
while (!eof_ && ((ch_ >= '0' && ch_ <= '9') || (std::toupper(ch_) >= 'A' && std::toupper(ch_) <= 'F'))) {
315+
while (!eof_ && ((ch_ >= '0' && ch_ <= '9') ||
316+
(std::toupper(static_cast<unsigned char>(ch_)) >= 'A' && std::toupper(static_cast<unsigned char>(ch_)) <= 'F'))) {
303317
ss << ch_;
304318
read();
305319
if (ch_ == '.') {
306320
auto offset = file_.tellg();
307321
read();
308322
if (ch_ == '.') {
309323
file_.seekg(offset);
324+
charNo_--;
310325
break;
311326
}
312327
ss << '.';
@@ -418,14 +433,20 @@ unique_ptr<const Token> Scanner::scanString() {
418433
stringstream ss;
419434
auto pos = current();
420435
read();
421-
while (ch_ != '"') {
436+
while (!eof_ && ch_ != '"') {
422437
ss << ch_;
423438
if (ch_ == '\\') {
424439
read();
425-
ss << ch_;
440+
if (!eof_) {
441+
ss << ch_;
442+
}
426443
}
427444
read();
428445
}
446+
if (eof_) {
447+
logger_.error(pos, "string literal not terminated.");
448+
throw ScannerError();
449+
}
429450
read();
430451
string str = unescape(ss.str());
431452
if (str.length() <= 1) {
@@ -435,34 +456,54 @@ unique_ptr<const Token> Scanner::scanString() {
435456
return make_unique<StringLiteralToken>(pos, current(), str);
436457
}
437458

438-
std::string Scanner::escape(std::string str) {
439-
boost::replace_all(str, "\0", "\\0");
440-
boost::replace_all(str, "\'", "\\'");
441-
boost::replace_all(str, "\"", "\\\"");
442-
boost::replace_all(str, "\?", "\\?");
443-
boost::replace_all(str, "\\", "\\\\");
444-
boost::replace_all(str, "\a", "\\a");
445-
boost::replace_all(str, "\b", "\\b");
446-
boost::replace_all(str, "\f", "\\f");
447-
boost::replace_all(str, "\n", "\\n");
448-
boost::replace_all(str, "\r", "\\r");
449-
boost::replace_all(str, "\t", "\\t");
450-
boost::replace_all(str, "\v", "\\v");
451-
return str;
459+
std::string Scanner::escape(const std::string& str) {
460+
std::string result;
461+
result.reserve(str.size() * 2);
462+
for (const char c : str) {
463+
switch (c) {
464+
case '\0': result += "\\0"; break;
465+
case '\'': result += "\\'"; break;
466+
case '"': result += "\\\""; break;
467+
case '?': result += "\\?"; break;
468+
case '\\': result += "\\\\"; break;
469+
case '\a': result += "\\a"; break;
470+
case '\b': result += "\\b"; break;
471+
case '\f': result += "\\f"; break;
472+
case '\n': result += "\\n"; break;
473+
case '\r': result += "\\r"; break;
474+
case '\t': result += "\\t"; break;
475+
case '\v': result += "\\v"; break;
476+
default: result += c; break;
477+
}
478+
}
479+
return result;
452480
}
453481

454-
std::string Scanner::unescape(std::string str) {
455-
boost::replace_all(str, "\\0", "\0");
456-
boost::replace_all(str, "\\'", "\'");
457-
boost::replace_all(str, "\\\"", "\"");
458-
boost::replace_all(str, "\\?", "\?");
459-
boost::replace_all(str, "\\\\", "\\");
460-
boost::replace_all(str, "\\a", "\a");
461-
boost::replace_all(str, "\\b", "\b");
462-
boost::replace_all(str, "\\f", "\f");
463-
boost::replace_all(str, "\\n", "\n");
464-
boost::replace_all(str, "\\r", "\r");
465-
boost::replace_all(str, "\\t", "\t");
466-
boost::replace_all(str, "\\v", "\v");
467-
return str;
482+
std::string Scanner::unescape(const std::string &str) {
483+
std::string result;
484+
result.reserve(str.size());
485+
size_t i = 0;
486+
while (i < str.size()) {
487+
if (str[i] == '\\' && i + 1 < str.size()) {
488+
switch (str[i + 1]) {
489+
case '0': result += '\0'; break;
490+
case '\'': result += '\''; break;
491+
case '"': result += '"'; break;
492+
case '?': result += '?'; break;
493+
case '\\': result += '\\'; break;
494+
case 'a': result += '\a'; break;
495+
case 'b': result += '\b'; break;
496+
case 'f': result += '\f'; break;
497+
case 'n': result += '\n'; break;
498+
case 'r': result += '\r'; break;
499+
case 't': result += '\t'; break;
500+
case 'v': result += '\v'; break;
501+
default: result += str[i]; i++; continue;
502+
}
503+
i += 2;
504+
} else {
505+
result += str[i++];
506+
}
507+
}
508+
return result;
468509
}

0 commit comments

Comments
 (0)