97 lines
3.8 KiB
Nim
97 lines
3.8 KiB
Nim
import frontend/parsing/parser
|
|
import frontend/parsing/lexer
|
|
|
|
|
|
import std/strformat
|
|
import std/sequtils
|
|
import std/strutils
|
|
|
|
|
|
type
|
|
Comparison* = ref object
|
|
# The result of a comparison between
|
|
# AST nodes
|
|
|
|
match*: bool # Did the comparison succeed?
|
|
reason*: string # If it failed, why?
|
|
exc*: ref Exception # If the comparison crashed with an error, this is the exception
|
|
inner*: Comparison # A comparison could fail because another, inner comparison failed
|
|
nodes*: tuple[provided, expected: ASTNode]
|
|
|
|
ASTChecker* = ref object
|
|
## An AST validator. Ensures the
|
|
## output of the parser matches
|
|
## the structure we expect
|
|
|
|
|
|
proc newASTChecker*: ASTChecker =
|
|
## Initializes a new, blank AST checker
|
|
new(result)
|
|
|
|
|
|
template ensureEqualType(self, other: ASTNode) = doAssert self.kind == other.kind, "cannot compare AST nodes of different types"
|
|
|
|
|
|
proc newComparison(match: bool, expected, provided: ASTNode, reason: string = "", exc: ref Exception = nil, inner: Comparison = nil): Comparison =
|
|
## Initializes a new comparison object
|
|
new(result)
|
|
result.match = match
|
|
result.nodes.expected = expected
|
|
result.nodes.provided = provided
|
|
result.reason = reason
|
|
result.exc = exc
|
|
result.inner = inner
|
|
|
|
|
|
proc compare(provided, expected: LiteralExpr): Comparison =
|
|
## Compares two literal expressions
|
|
## for structural equality
|
|
ensureEqualType(provided, expected)
|
|
result = newComparison(true, expected, provided)
|
|
case provided.kind:
|
|
of infExpr, nanExpr, falseExpr, trueExpr:
|
|
return
|
|
of strExpr, intExpr, hexExpr, octExpr, binExpr:
|
|
if provided.literal.kind != expected.literal.kind:
|
|
result.match = false
|
|
result.reason = &"expected token of type {expected.literal.kind}, got {provided.literal.kind}"
|
|
elif provided.literal.lexeme != expected.literal.lexeme:
|
|
result.match = false
|
|
result.reason = &"expected lexeme to be {expected.literal.lexeme.escape()}, got {provided.literal.lexeme.escape()}"
|
|
else:
|
|
raise newException(ValueError, &"literal comparison attempted with non-literal nodes of type {provided.kind}")
|
|
|
|
|
|
proc compare(provided, expected: ASTNode): Comparison =
|
|
## Compares two generic AST nodes for
|
|
## structural equality
|
|
ensureEqualType(provided, expected)
|
|
if provided.isLiteral():
|
|
return compare(LiteralExpr(provided), LiteralExpr(expected))
|
|
else:
|
|
raise newException(ValueError, &"comparisons for nodes of type {provided.kind} are not supported yet")
|
|
|
|
|
|
proc check*(self: ASTChecker, code: string, structure: ParseTree): Comparison =
|
|
## Ensures the given source code parses into the given structure
|
|
try:
|
|
result = newComparison(true, nil, nil)
|
|
var
|
|
tokenizer = newLexer()
|
|
parser = newParser()
|
|
tokens = tokenizer.lex(code, "test")
|
|
tree = parser.parse(tokens, "test", tokenizer.getLines(), tokenizer.getSource())
|
|
if len(tree) != len(structure):
|
|
result.match = false
|
|
result.reason = &"Number of provided nodes ({structure.len()}) does not match number of returned nodes ({tree.len()})"
|
|
for (provided, expected) in zip(tree, structure):
|
|
let cmp = compare(provided, expected)
|
|
if not cmp.match:
|
|
result.inner = cmp
|
|
result.reason = "Comparison failed, see the inner attribute for more details"
|
|
break
|
|
except LexingError:
|
|
return Comparison(match: false, reason: "Tokenization failed", exc: getCurrentException())
|
|
except ParseError:
|
|
return Comparison(match: false, reason: "Parsing failed", exc: getCurrentException())
|