Compare commits

...

2 Commits

4 changed files with 159 additions and 70 deletions

View File

@ -401,7 +401,10 @@ proc parseString(self: Lexer, delimiter: string, mode: string = "single") =
if mode == "multi":
self.incLine()
else:
self.error("unexpected EOL while parsing string literal")
if delimiter == "'":
self.error("unexpected EOL while parsing character literal")
else:
self.error("unexpected EOL while parsing string literal")
if mode in ["raw", "multi"]:
discard self.step()
elif self.match("\\"):

View File

@ -12,12 +12,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
## Utilities to print formatted error messages to stderr
## Utilities to format peon exceptions into human-readable error messages
## and print them
import frontend/compiler/typechecker
import frontend/parsing/parser
import frontend/parsing/lexer
import errors
export errors
import std/os
import std/terminal
@ -25,24 +28,26 @@ import std/strutils
import std/strformat
proc printError(file, line: string, lineNo: int, pos: tuple[start, stop: int], fn: Declaration, msg: string) =
## Internal helper to print a formatted error message
## to stderr
stderr.styledWrite(fgRed, styleBright, "Error in ", fgYellow, &"{file}:{lineNo}:{pos.start}")
proc formatError*(outFile = stderr, file, line: string, lineNo: int, pos: tuple[start, stop: int], fn: Declaration, msg: string, includeSource = true) =
## Helper to write a formatted error message to the given file object
outFile.styledWrite(fgRed, styleBright, "Error in ", fgYellow, &"{file}:{lineNo}:{pos.start}")
if not fn.isNil() and fn.kind == funDecl:
# Error occurred inside a (named) function
stderr.styledWrite(fgRed, styleBright, " in function ", fgYellow, FunDecl(fn).name.token.lexeme)
stderr.styledWriteLine(styleBright, fgDefault, ": ", msg)
if line.len() > 0:
stderr.styledWrite(fgRed, styleBright, "Source line: ", resetStyle, fgDefault, line[0..<pos.start])
outFile.styledWriteLine(styleBright, fgDefault, ": ", msg)
if line.len() > 0 and includeSource:
# Print the line where the error occurred and underline the exact node that caused
# the error. Might be inaccurate, but definitely better than nothing
outFile.styledWrite(fgRed, styleBright, "Source line: ", resetStyle, fgDefault, line[0..<pos.start])
if pos.stop == line.len():
stderr.styledWrite(fgRed, styleUnderscore, line[pos.start..<pos.stop])
stderr.styledWriteLine(fgDefault, line[pos.stop..^1])
outFile.styledWrite(fgRed, styleUnderscore, line[pos.start..<pos.stop])
outFile.styledWriteLine(fgDefault, line[pos.stop..^1])
else:
stderr.styledWrite(fgRed, styleUnderscore, line[pos.start..pos.stop])
stderr.styledWriteLine(fgDefault, line[pos.stop + 1..^1])
outFile.styledWrite(fgRed, styleUnderscore, line[pos.start..pos.stop])
outFile.styledWriteLine(fgDefault, line[pos.stop + 1..^1])
proc print*(exc: TypeCheckError) =
proc print*(exc: TypeCheckError, includeSource = true) =
## Prints a formatted error message
## for type checking errors to stderr
var file = exc.file
@ -51,10 +56,10 @@ proc print*(exc: TypeCheckError) =
of -1: discard
of 0: contents = exc.instance.getSource().strip(chars={'\n'}).splitLines()[exc.line]
else: contents = exc.instance.getSource().strip(chars={'\n'}).splitLines()[exc.line - 1]
printError(file, contents, exc.line, exc.node.getRelativeBoundaries(), exc.function, exc.msg)
formatError(stderr, file, contents, exc.line, exc.node.getRelativeBoundaries(), exc.function, exc.msg, includeSource)
proc print*(exc: ParseError) =
proc print*(exc: ParseError, includeSource = true) =
## Prints a formatted error message
## for parsing errors to stderr
var file = exc.file
@ -65,10 +70,10 @@ proc print*(exc: ParseError) =
contents = exc.parser.getSource().strip(chars={'\n'}).splitLines()[exc.line - 1]
else:
contents = ""
printError(file, contents, exc.line, exc.token.relPos, nil, exc.msg)
formatError(stderr, file, contents, exc.line, exc.token.relPos, nil, exc.msg, includeSource)
proc print*(exc: LexingError) =
proc print*(exc: LexingError, includeSource = true) =
## Prints a formatted error message
## for lexing errors to stderr
var file = exc.file
@ -79,5 +84,5 @@ proc print*(exc: LexingError) =
contents = exc.lexer.getSource().strip(chars={'\n'}).splitLines()[exc.line - 1]
else:
contents = ""
printError(file, contents, exc.line, exc.pos, nil, exc.msg)
formatError(stderr, file, contents, exc.line, exc.pos, nil, exc.msg, includeSource)

View File

@ -25,44 +25,51 @@ import util/symbols
type
TestStatus* = enum
## Test status enumeration
Init, Running, Success,
Failed, Crashed,
TimedOut, Skipped
TestKind* = enum
## Test type enumeration
Tokenizer, Parser, TypeChecker,
Runtime
TestRunner = proc (suite: TestSuite, test: Test)
TestOutcome = tuple[error: bool, exc: ref Exception]
# Represents a test outcome. The exc field contains
# the exception raised during the test, if any. The
# error field indicates whether the test errored out
# or not. If exc is non-null and error is false, this
# means the error was expected behavior
TestOutcome = tuple[error: bool, exc: ref Exception, line: int, location: tuple[start, stop: int]]
Test* {.inheritable.} = ref object
skip*: bool
name*: string
kind*: TestKind
source*: string
status*: TestStatus
expected*: TestStatus
outcome*: TestOutcome
runnerFunc: TestRunner
## A generic test object
skip*: bool # Skip running this test if true
name*: string # Test name. Only useful for displaying purposes
kind*: TestKind # Test kind (tokenizer, parser, compiler, etc.)
source*: string # The source input of the test. Usually peon code
status*: TestStatus # The test's current state
expected*: TestStatus # The test's expected final state after run()
outcome*: TestOutcome # The test's outcome
runnerFunc: TestRunner # The test's internal runner function
TokenizerTest* = ref object of Test
## A tokenization test. Allows to specify
## a desired error message and error location
## upon tokenization failure
message: string
location: tuple[start, stop: int]
line: int
lexer: Lexer
TestSuite* = ref object
## A suite of tests
tests*: seq[Test]
proc `$`(self: tuple[error: bool, exc: ref Exception]): string =
if self.exc.isNil():
result = &"Outcome(error={self.error}, exc=nil)"
else:
var name = ($self.exc.name).split(":")[0]
result = &"Outcome(error={self.error}, exc=Error(name='{name}', msg='{self.exc.msg}')"
proc `$`(self: tuple[start, stop: int]): string =
if self == (-1, -1):
result = "none"
@ -70,32 +77,40 @@ proc `$`(self: tuple[start, stop: int]): string =
result = &"Location(start={self.start}, stop={self.stop})"
proc `$`(self: TestOutcome): string =
result &= "Outcome(error={self.error}"
if self.exc.isNil():
result &= &", exc=nil"
else:
var name = ($self.exc.name).split(":")[0]
result = &"exc=Error(name='{name}', msg='{self.exc.msg}'"
if self.line != -1:
result &= &", line={self.line}"
result &= &", location={self.location})"
proc `$`*(self: Test): string =
case self.kind:
of Tokenizer:
var self = TokenizerTest(self)
return &"TokenizerTest(name='{self.name}', status={self.status}, outcome={self.outcome}, source='{self.source}', location={self.location}, message='{self.message}')"
return &"TokenizerTest(name='{self.name}', status={self.status}, outcome={self.outcome}, source='{self.source.escape()}', location={self.location}, message='{self.message}')"
else:
# TODO
return ""
proc getTestDetails(self: Test): string =
doAssert self.status != Init
case self.kind:
of Tokenizer:
var self = TokenizerTest(self)
result = &"expecting "
else:
# TODO
discard
proc setup(self: TokenizerTest) =
self.lexer = newLexer()
self.lexer.fillSymbolTable()
proc tokenizeSucceedsRunner(suite: TestSuite, test: Test) =
## Runs a tokenitazion test that is expected to succeed
var test = TokenizerTest(test)
test.setup()
try:
var tokenizer = newLexer()
tokenizer.fillSymbolTable()
discard tokenizer.lex(test.source, "<string>")
discard test.lexer.lex(test.source, test.name)
except LexingError:
test.status = Failed
test.outcome.error = true
@ -110,14 +125,17 @@ proc tokenizeSucceedsRunner(suite: TestSuite, test: Test) =
proc tokenizeFailsRunner(suite: TestSuite, test: Test) =
## Runs a tokenitazion test that is expected to fail
## and checks that it does so in the way we expect
var test = TokenizerTest(test)
test.setup()
try:
var tokenizer = newLexer()
tokenizer.fillSymbolTable()
discard tokenizer.lex(test.source, "<string>")
discard test.lexer.lex(test.source, test.name)
except LexingError:
var exc = LexingError(getCurrentException())
if exc.pos == test.location and exc.msg == test.message:
test.outcome.location = exc.pos
test.outcome.line = exc.line
if exc.pos == test.location and exc.line == test.line and exc.msg == test.message:
test.status = Success
else:
test.status = Failed
@ -132,54 +150,68 @@ proc tokenizeFailsRunner(suite: TestSuite, test: Test) =
test.status = Failed
proc newTestSuite*: TestSuite = new(result)
proc newTestSuite*: TestSuite =
## Creates a new test suite
new(result)
proc addTest*(self: TestSuite, test: Test) =
## Adds a test to the test suite
self.tests.add(test)
proc addTests*(self: TestSuite, tests: openarray[Test]) =
## Adds the given tests to the test suite
for test in tests:
self.addTest(test)
proc removeTest*(self: TestSuite, test: Test) =
## Removes the given test from the test suite
self.tests.delete(self.tests.find(test))
proc removeTests*(self: TestSuite, tests: openarray[Test]) =
## Removes the given tests from the test suite
for test in tests:
self.removeTest(test)
proc newTokenizeTest(name, source: string): TokenizerTest =
proc newTokenizeTest(name, source: string, skip = false): TokenizerTest =
new(result)
result.name = name
result.kind = Tokenizer
result.status = Init
result.source = source
result.skip = skip
result.line = -1
result.location = (-1, -1)
result.message = ""
proc testTokenizeSucceeds*(name, source: string, skip = false): Test =
var test = newTokenizeTest(name, source)
## Creates a new tokenizer test that is expected to succeed
var test = newTokenizeTest(name, source, skip)
test.runnerFunc = tokenizeSucceedsRunner
test.message = ""
test.location = (-1 , -1)
result = Test(test)
result.skip = skip
result.expected = Success
proc testTokenizeFails*(name, source: string, message: string, location: tuple[start, stop: int], skip = false): Test =
var test = newTokenizeTest(name, source)
proc testTokenizeFails*(name, source: string, message: string, line: int, location: tuple[start, stop: int], skip = false): Test =
## Creates a new tokenizer test that is expected to fail with the
## given error message and at the given location
var test = newTokenizeTest(name, source, skip)
test.runnerFunc = tokenizeFailsRunner
test.message = message
test.location = location
test.line = line
result = Test(test)
result.skip = skip
result.expected = Failed
proc run*(self: TestSuite) =
## Runs the test suite to completion,
## sequentially
for test in self.tests:
if test.skip:
test.status = Skipped
@ -188,9 +220,42 @@ proc run*(self: TestSuite) =
proc successful*(self: TestSuite): bool =
## Returns whether the test suite completed
## successfully or not. If called before run(),
## this function returns false. Skipped tests
## do not affect the outcome of this function
result = true
for test in self.tests:
if test.status in [Skipped, Success]:
continue
result = false
break
break
proc getExpectedException(self: TokenizerTest): ref Exception =
## Gets the exception that we expect to be
## raised by the test. Could be nil if we
## expect no errors
if self.expected == Success:
return nil
return LexingError(msg: self.message, line: self.line, file: self.name, lexer: self.lexer, pos: self.location)
proc getExpectedOutcome(self: TokenizerTest): TestOutcome =
## Gets the expected outcome of a tokenization test
if self.expected == Success:
return (false, self.getExpectedException(), -1, (-1, -1))
else:
return (false, self.getExpectedException, self.line, self.location)
proc getExpectedOutcome*(self: Test): TestOutcome =
## Returns the expected outcome of a test
doAssert self.expected in [Success, Failed], "expected outcome is neither Success nor Failed: wtf?"
case self.kind:
of Tokenizer:
return TokenizerTest(self).getExpectedOutcome()
else:
# TODO
discard

View File

@ -1,7 +1,11 @@
import util/testing
import util/fmterr
import frontend/parsing/lexer
import std/strformat
import std/strutils
when isMainModule:
@ -9,18 +13,30 @@ when isMainModule:
suite.addTests(
[
testTokenizeSucceeds("emptyFile", ""),
testTokenizeFails("unterminatedChar", "'", "unexpected EOF while parsing character literal", (0, 0)),
testTokenizeFails("emptyChar", "''", "character literal cannot be of length zero", (0, 1)),
testTokenizeFails("unterminatedString", "\"", "unexpected EOF while parsing string literal", (0, 0)),
testTokenizeSucceeds("emptyString", "\"\"")
testTokenizeSucceeds("newLine", "\n"),
testTokenizeSucceeds("emptyString", "\"\""),
testTokenizeFails("unterminatedChar", "'", "unexpected EOF while parsing character literal", line=1, location=(0, 0)),
testTokenizeFails("emptyChar", "''", "character literal cannot be of length zero", line=1, location=(0, 1)),
testTokenizeFails("unterminatedString", "\"", "unexpected EOF while parsing string literal", line=1, location=(0, 0)),
testTokenizeFails("unterminatedCharWithExtraContent", "'\n;", "unexpected EOL while parsing character literal", line=1, location=(0, 1)),
testTokenizeFails("unterminatedStringWithExtraContent", "\"\n;", "unexpected EOL while parsing string literal", line=1, location=(0, 1)),
]
)
suite.run()
echo "Tokenization test results: "
for test in suite.tests:
echo &" - {test.name} -> {test.status}"
if test.status != Success:
echo &" Details: {test}"
if test.status in [Failed, Crashed]:
echo &" Details:"
echo &" - Source: {test.source.escape()}"
echo &" - Outcome: {test.outcome}"
echo &" - Expected state: {test.expected} "
echo &" - Expected outcome: {test.getExpectedOutcome()}"
if not test.outcome.exc.isNil():
echo &"\n Formatted error message follows\n"
print(LexingError(test.outcome.exc))
echo "\n Formatted error message ends here\n"
if suite.successful():
quit(0)
quit(-1)