Skip to content

Commit ea9d96d

Browse files
committed
Added better error reporting for syntax errors
1 parent 9163c44 commit ea9d96d

File tree

3 files changed

+96
-34
lines changed

3 files changed

+96
-34
lines changed

Sources/SLisp/Repl.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public class Repl {
7373
return "Error: \(message)"
7474
} catch let LispError.lexer(msg:message) {
7575
ln.addHistory(input)
76-
return "Syntax Error: \(message)"
76+
return message
7777
} catch let LispError.runtimeForm(msg: message, form: form) {
7878
ln.addHistory(input)
7979
var retMsg = "Error: \(message)"

Sources/SLispCore/Lexer.swift

+79-26
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,48 @@
2626

2727
import Foundation
2828

29+
public struct TokenPosition {
30+
let line: Int
31+
let column: Int
32+
let source: String
33+
34+
var sourceLine: String {
35+
return String(source.split(separator: "\n")[line])
36+
}
37+
38+
var tokenMarker: String {
39+
// Creates a "^" marker underneath the source line pointing to the token
40+
var output = ""
41+
42+
// Add marker at appropriate column
43+
for _ in 0..<(column - 1) {
44+
output += " "
45+
}
46+
47+
output += "^"
48+
return output
49+
}
50+
}
2951

30-
public enum TokenType: Equatable {
31-
case lParen
32-
case rParen
33-
case lBrace
34-
case rBrace
35-
case symbol(String)
36-
case float(Double)
37-
case integer(Int)
38-
case string(String)
52+
public enum TokenType {
53+
case lParen(TokenPosition)
54+
case rParen(TokenPosition)
55+
case lBrace(TokenPosition)
56+
case rBrace(TokenPosition)
57+
case symbol(TokenPosition, String)
58+
case float(TokenPosition, Double)
59+
case integer(TokenPosition, Int)
60+
case string(TokenPosition, String)
3961
}
4062

4163
public func ==(a: TokenType, b: TokenType) -> Bool {
4264
switch (a, b) {
4365
case (.lParen, .lParen): return true
4466
case (.rParen, .rParen): return true
45-
case (.symbol(let a), .symbol(let b)) where a == b: return true
46-
case (.float(let a), .float(let b)) where a == b: return true
47-
case (.integer(let a), .integer(let b)) where a == b: return true
48-
case (.string(let a), .string(let b)) where a == b: return true
67+
case (.symbol(_, let a), .symbol(_, let b)) where a == b: return true
68+
case (.float(_, let a), .float(_, let b)) where a == b: return true
69+
case (.integer(_, let a), .integer(_, let b)) where a == b: return true
70+
case (.string(_, let a), .string(_, let b)) where a == b: return true
4971
default: return false
5072
}
5173
}
@@ -77,7 +99,7 @@ class LParenTokenMatcher: TokenMatcher {
7799
static func getToken(_ stream: StringStream) -> TokenType? {
78100
if isMatch(stream) {
79101
stream.advanceCharacter()
80-
return TokenType.lParen
102+
return TokenType.lParen(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str))
81103
}
82104
return nil
83105
}
@@ -91,7 +113,7 @@ class RParenTokenMatcher: TokenMatcher {
91113
static func getToken(_ stream: StringStream) -> TokenType? {
92114
if isMatch(stream) {
93115
stream.advanceCharacter()
94-
return TokenType.rParen
116+
return TokenType.rParen(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str))
95117
}
96118
return nil
97119
}
@@ -105,7 +127,7 @@ class LBraceTokenMatcher: TokenMatcher {
105127
static func getToken(_ stream: StringStream) -> TokenType? {
106128
if isMatch(stream) {
107129
stream.advanceCharacter()
108-
return TokenType.lBrace
130+
return TokenType.lBrace(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str))
109131
}
110132
return nil
111133
}
@@ -119,7 +141,7 @@ class RBraceTokenMatcher: TokenMatcher {
119141
static func getToken(_ stream: StringStream) -> TokenType? {
120142
if isMatch(stream) {
121143
stream.advanceCharacter()
122-
return TokenType.rBrace
144+
return TokenType.rBrace(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str))
123145
}
124146
return nil
125147
}
@@ -145,7 +167,7 @@ class SymbolMatcher: TokenMatcher {
145167
}
146168
}
147169

148-
return TokenType.symbol(tok)
170+
return TokenType.symbol(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str), tok)
149171
}
150172
return nil
151173
}
@@ -193,7 +215,13 @@ class StringMatcher: TokenMatcher {
193215
stream.advanceCharacter()
194216

195217
guard let escapeChar = stream.currentCharacter else {
196-
throw LispError.lexer(msg: "Error in string: expected escape character")
218+
let pos = TokenPosition(line: stream.currentLine, column: stream.currentColumn + 1, source: stream.str)
219+
let msg = """
220+
\(pos.line):\(pos.column): Expected escape character:
221+
\(pos.sourceLine)
222+
\(pos.tokenMarker)
223+
"""
224+
throw LispError.lexer(msg: msg)
197225
}
198226

199227
let escapeResult: String
@@ -218,10 +246,17 @@ class StringMatcher: TokenMatcher {
218246
throw LispError.lexer(msg: "Error in string: invalid hex escape sequence: \(String([h1, h2]))")
219247
}
220248
escapeResult = String(Character(UnicodeScalar(hexValue)))
221-
249+
case "\"":
250+
escapeResult = "\""
222251

223252
default:
224-
throw LispError.lexer(msg: "Unknown escape character in string: \\\(escapeChar)")
253+
let pos = TokenPosition(line: stream.currentLine, column: stream.currentColumn + 1, source: stream.str)
254+
let msg = """
255+
\(pos.line):\(pos.column): Unknown escape character in string: \\\(escapeChar)
256+
\(pos.sourceLine)
257+
\(pos.tokenMarker)
258+
"""
259+
throw LispError.lexer(msg: msg)
225260
}
226261

227262
tok += escapeResult
@@ -239,7 +274,7 @@ class StringMatcher: TokenMatcher {
239274

240275
stream.advanceCharacter()
241276

242-
return TokenType.string(tok)
277+
return TokenType.string(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str), tok)
243278
}
244279

245280
return nil
@@ -294,12 +329,12 @@ class NumberMatcher: TokenMatcher {
294329
guard let num = Double(tok) else {
295330
throw LispError.lexer(msg: "\(tok) is not a valid floating point number.")
296331
}
297-
return TokenType.float(num)
332+
return TokenType.float(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str), num)
298333
} else {
299334
guard let num = Int(tok) else {
300335
throw LispError.lexer(msg: "\(tok) is not a valid number.")
301336
}
302-
return TokenType.integer(num)
337+
return TokenType.integer(TokenPosition(line: stream.currentLine, column: stream.currentColumn, source: stream.str), num)
303338
}
304339

305340
}
@@ -341,6 +376,9 @@ class StringStream {
341376
var currentCharacterIdx: String.Index
342377
var nextCharacterIdx: String.Index?
343378
var characterCount: Int
379+
380+
var currentLine: Int
381+
var currentColumn: Int
344382

345383
init(source: String) {
346384
str = source
@@ -350,12 +388,22 @@ class StringStream {
350388
nextCharacterIdx = str.index(after: currentCharacterIdx)
351389
currentCharacter = str.characters[currentCharacterIdx]
352390

391+
currentLine = 0
392+
currentColumn = 0
393+
353394
if str.count > 1 {
354395
nextCharacter = str[nextCharacterIdx!]
355396
}
356397
}
357398

358399
func advanceCharacter() {
400+
if currentCharacter != nil && currentCharacter! == "\n" {
401+
currentLine += 1
402+
currentColumn = 0
403+
} else {
404+
currentColumn += 1
405+
}
406+
359407
position += 1
360408

361409
if position >= characterCount
@@ -453,11 +501,16 @@ class Tokenizer {
453501
}
454502
} else {
455503
if count == 0 {
456-
throw LispError.lexer(msg: "Unrecognized character '\(stream.currentCharacter ?? " ".first!)'")
504+
let pos = TokenPosition(line: stream.currentLine, column: stream.currentColumn + 1, source: stream.str)
505+
let msg = """
506+
\(pos.line):\(pos.column): Unrecognized character '\(stream.currentCharacter ?? " ".first!)':
507+
\t\(pos.sourceLine)
508+
\t\(pos.tokenMarker)
509+
"""
510+
throw LispError.lexer(msg: msg)
457511
}
458512
}
459513

460514
return try getNextToken()
461515
}
462-
463516
}

Sources/SLispCore/Reader.swift

+16-7
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class Reader {
5353
return try read_list()
5454
case .lBrace:
5555
return try read_dict()
56-
case .symbol(let str):
56+
case .symbol(_, let str):
5757

5858
// Handle keys
5959
if str.hasPrefix(":") {
@@ -103,15 +103,24 @@ public class Reader {
103103
}
104104

105105
return .symbol(str)
106-
case .string(let str):
106+
case .string(_, let str):
107107
return .string(str)
108-
case .float(let num):
108+
case .float(_, let num):
109109
return .number(.float(num))
110-
case .integer(let num):
110+
case .integer(_, let num):
111111
return .number(.integer(num))
112-
default:
113-
Swift.print("Error while reading token \(token) at index \(pos)")
114-
return .nil
112+
case .rParen(let tokenPos):
113+
throw LispError.lexer(msg: """
114+
\(tokenPos.line):\(tokenPos.column): Unexpected ')'
115+
\t\(tokenPos.sourceLine)
116+
\t\(tokenPos.tokenMarker)
117+
""")
118+
case .rBrace(let tokenPos):
119+
throw LispError.lexer(msg: """
120+
\(tokenPos.line):\(tokenPos.column): Unexpected '}'
121+
\t\(tokenPos.sourceLine)
122+
\t\(tokenPos.tokenMarker)
123+
""")
115124
}
116125
}
117126

0 commit comments

Comments
 (0)