made test green again

This commit is contained in:
Araq 2015-08-02 01:16:47 +02:00
commit 5e325bc9f9

View file

@ -2,344 +2,344 @@ discard """
output: "Success" output: "Success"
""" """
# Ref: # Ref:
# http://nim-lang.org/macros.html # http://nim-lang.org/macros.html
# http://nim-lang.org/parseutils.html # http://nim-lang.org/parseutils.html
# Imports # Imports
import tables, parseutils, macros, strutils import tables, parseutils, macros, strutils
import annotate import annotate
export annotate export annotate
# Fields # Fields
const identChars = {'a'..'z', 'A'..'Z', '0'..'9', '_'} const identChars = {'a'..'z', 'A'..'Z', '0'..'9', '_'}
# Procedure Declarations # Procedure Declarations
proc parse_template(node: NimNode, value: string) {.compiletime.} proc parse_template(node: NimNode, value: string) {.compiletime.}
# Procedure Definitions # Procedure Definitions
proc substring(value: string, index: int, length = -1): string {.compiletime.} = proc substring(value: string, index: int, length = -1): string {.compiletime.} =
## Returns a string at most `length` characters long, starting at `index`. ## Returns a string at most `length` characters long, starting at `index`.
return if length < 0: value.substr(index) return if length < 0: value.substr(index)
elif length == 0: "" elif length == 0: ""
else: value.substr(index, index + length-1) else: value.substr(index, index + length-1)
proc parse_thru_eol(value: string, index: int): int {.compiletime.} = proc parse_thru_eol(value: string, index: int): int {.compiletime.} =
## Reads until and past the end of the current line, unless ## Reads until and past the end of the current line, unless
## a non-whitespace character is encountered first ## a non-whitespace character is encountered first
var remainder: string var remainder: string
var read = value.parseUntil(remainder, {0x0A.char}, index) var read = value.parseUntil(remainder, {0x0A.char}, index)
if remainder.skipWhitespace() == read: if remainder.skipWhitespace() == read:
return read + 1 return read + 1
proc trim_after_eol(value: var string) {.compiletime.} = proc trim_after_eol(value: var string) {.compiletime.} =
## Trims any whitespace at end after \n ## Trims any whitespace at end after \n
var toTrim = 0 var toTrim = 0
for i in countdown(value.len-1, 0): for i in countdown(value.len-1, 0):
# If \n, return # If \n, return
if value[i] in [' ', '\t']: inc(toTrim) if value[i] in [' ', '\t']: inc(toTrim)
else: break else: break
if toTrim > 0: if toTrim > 0:
value = value.substring(0, value.len - toTrim) value = value.substring(0, value.len - toTrim)
proc trim_eol(value: var string) {.compiletime.} = proc trim_eol(value: var string) {.compiletime.} =
## Removes everything after the last line if it contains nothing but whitespace ## Removes everything after the last line if it contains nothing but whitespace
for i in countdown(value.len - 1, 0): for i in countdown(value.len - 1, 0):
# If \n, trim and return # If \n, trim and return
if value[i] == 0x0A.char: if value[i] == 0x0A.char:
value = value.substr(0, i) value = value.substr(0, i)
break break
# This is the first character # This is the first character
if i == 0: if i == 0:
value = "" value = ""
break break
# Skip change # Skip change
if not (value[i] in [' ', '\t']): break if not (value[i] in [' ', '\t']): break
proc detect_indent(value: string, index: int): int {.compiletime.} = proc detect_indent(value: string, index: int): int {.compiletime.} =
## Detects how indented the line at `index` is. ## Detects how indented the line at `index` is.
# Seek to the beginning of the line. # Seek to the beginning of the line.
var lastChar = index var lastChar = index
for i in countdown(index, 0): for i in countdown(index, 0):
if value[i] == 0x0A.char: if value[i] == 0x0A.char:
# if \n, return the indentation level # if \n, return the indentation level
return lastChar - i return lastChar - i
elif not (value[i] in [' ', '\t']): elif not (value[i] in [' ', '\t']):
# if non-whitespace char, decrement lastChar # if non-whitespace char, decrement lastChar
dec(lastChar) dec(lastChar)
proc parse_thru_string(value: string, i: var int, strType = '"') {.compiletime.} = proc parse_thru_string(value: string, i: var int, strType = '"') {.compiletime.} =
## Parses until ending " or ' is reached. ## Parses until ending " or ' is reached.
inc(i) inc(i)
if i < value.len-1: if i < value.len-1:
inc(i, value.skipUntil({'\\', strType}, i)) inc(i, value.skipUntil({'\\', strType}, i))
proc parse_to_close(value: string, index: int, open='(', close=')', opened=0): int {.compiletime.} = proc parse_to_close(value: string, index: int, open='(', close=')', opened=0): int {.compiletime.} =
## Reads until all opened braces are closed ## Reads until all opened braces are closed
## ignoring any strings "" or '' ## ignoring any strings "" or ''
var remainder = value.substring(index) var remainder = value.substring(index)
var open_braces = opened var open_braces = opened
result = 0 result = 0
while result < remainder.len: while result < remainder.len:
var c = remainder[result] var c = remainder[result]
if c == open: inc(open_braces) if c == open: inc(open_braces)
elif c == close: dec(open_braces) elif c == close: dec(open_braces)
elif c == '"': remainder.parse_thru_string(result) elif c == '"': remainder.parse_thru_string(result)
elif c == '\'': remainder.parse_thru_string(result, '\'') elif c == '\'': remainder.parse_thru_string(result, '\'')
if open_braces == 0: break if open_braces == 0: break
else: inc(result) else: inc(result)
iterator parse_stmt_list(value: string, index: var int): string = iterator parse_stmt_list(value: string, index: var int): string =
## Parses unguided ${..} block ## Parses unguided ${..} block
var read = value.parse_to_close(index, open='{', close='}') var read = value.parse_to_close(index, open='{', close='}')
var expressions = value.substring(index + 1, read - 1).split({ ';', 0x0A.char }) var expressions = value.substring(index + 1, read - 1).split({ ';', 0x0A.char })
for expression in expressions: for expression in expressions:
let value = expression.strip let value = expression.strip
if value.len > 0: if value.len > 0:
yield value yield value
#Increment index & parse thru EOL #Increment index & parse thru EOL
inc(index, read + 1) inc(index, read + 1)
inc(index, value.parse_thru_eol(index)) inc(index, value.parse_thru_eol(index))
iterator parse_compound_statements(value, identifier: string, index: int): string = iterator parse_compound_statements(value, identifier: string, index: int): string =
## Parses through several statements, i.e. if {} elif {} else {} ## Parses through several statements, i.e. if {} elif {} else {}
## and returns the initialization of each as an empty statement ## and returns the initialization of each as an empty statement
## i.e. if x == 5 { ... } becomes if x == 5: nil. ## i.e. if x == 5 { ... } becomes if x == 5: nil.
template get_next_ident(expected): stmt = template get_next_ident(expected): stmt =
var nextIdent: string var nextIdent: string
discard value.parseWhile(nextIdent, {'$'} + identChars, i) discard value.parseWhile(nextIdent, {'$'} + identChars, i)
var next: string var next: string
var read: int var read: int
if nextIdent == "case": if nextIdent == "case":
# We have to handle case a bit differently # We have to handle case a bit differently
read = value.parseUntil(next, '$', i) read = value.parseUntil(next, '$', i)
inc(i, read) inc(i, read)
yield next.strip(leading=false) & "\n" yield next.strip(leading=false) & "\n"
else: else:
read = value.parseUntil(next, '{', i) read = value.parseUntil(next, '{', i)
if nextIdent in expected: if nextIdent in expected:
inc(i, read) inc(i, read)
# Parse until closing }, then skip whitespace afterwards # Parse until closing }, then skip whitespace afterwards
read = value.parse_to_close(i, open='{', close='}') read = value.parse_to_close(i, open='{', close='}')
inc(i, read + 1) inc(i, read + 1)
inc(i, value.skipWhitespace(i)) inc(i, value.skipWhitespace(i))
yield next & ": nil\n" yield next & ": nil\n"
else: break else: break
var i = index var i = index
while true: while true:
# Check if next statement would be valid, given the identifier # Check if next statement would be valid, given the identifier
if identifier in ["if", "when"]: if identifier in ["if", "when"]:
get_next_ident([identifier, "$elif", "$else"]) get_next_ident([identifier, "$elif", "$else"])
elif identifier == "case": elif identifier == "case":
get_next_ident(["case", "$of", "$elif", "$else"]) get_next_ident(["case", "$of", "$elif", "$else"])
elif identifier == "try": elif identifier == "try":
get_next_ident(["try", "$except", "$finally"]) get_next_ident(["try", "$except", "$finally"])
proc parse_complex_stmt(value, identifier: string, index: var int): NimNode {.compiletime.} = proc parse_complex_stmt(value, identifier: string, index: var int): NimNode {.compiletime.} =
## Parses if/when/try /elif /else /except /finally statements ## Parses if/when/try /elif /else /except /finally statements
# Build up complex statement string # Build up complex statement string
var stmtString = newString(0) var stmtString = newString(0)
var numStatements = 0 var numStatements = 0
for statement in value.parse_compound_statements(identifier, index): for statement in value.parse_compound_statements(identifier, index):
if statement[0] == '$': stmtString.add(statement.substr(1)) if statement[0] == '$': stmtString.add(statement.substr(1))
else: stmtString.add(statement) else: stmtString.add(statement)
inc(numStatements) inc(numStatements)
# Parse stmt string # Parse stmt string
result = parseExpr(stmtString) result = parseExpr(stmtString)
var resultIndex = 0 var resultIndex = 0
# Fast forward a bit if this is a case statement # Fast forward a bit if this is a case statement
if identifier == "case": if identifier == "case":
inc(resultIndex) inc(resultIndex)
while resultIndex < numStatements: while resultIndex < numStatements:
# Detect indentation # Detect indentation
let indent = detect_indent(value, index) let indent = detect_indent(value, index)
# Parse until an open brace `{` # Parse until an open brace `{`
var read = value.skipUntil('{', index) var read = value.skipUntil('{', index)
inc(index, read + 1) inc(index, read + 1)
# Parse through EOL # Parse through EOL
inc(index, value.parse_thru_eol(index)) inc(index, value.parse_thru_eol(index))
# Parse through { .. } # Parse through { .. }
read = value.parse_to_close(index, open='{', close='}', opened=1) read = value.parse_to_close(index, open='{', close='}', opened=1)
# Add parsed sub-expression into body # Add parsed sub-expression into body
var body = newStmtList() var body = newStmtList()
var stmtString = value.substring(index, read) var stmtString = value.substring(index, read)
trim_after_eol(stmtString) trim_after_eol(stmtString)
stmtString = reindent(stmtString, indent) stmtString = reindent(stmtString, indent)
parse_template(body, stmtString) parse_template(body, stmtString)
inc(index, read + 1) inc(index, read + 1)
# Insert body into result # Insert body into result
var stmtIndex = macros.high(result[resultIndex]) var stmtIndex = result[resultIndex].len-1
result[resultIndex][stmtIndex] = body result[resultIndex][stmtIndex] = body
# Parse through EOL again & increment result index # Parse through EOL again & increment result index
inc(index, value.parse_thru_eol(index)) inc(index, value.parse_thru_eol(index))
inc(resultIndex) inc(resultIndex)
proc parse_simple_statement(value: string, index: var int): NimNode {.compiletime.} = proc parse_simple_statement(value: string, index: var int): NimNode {.compiletime.} =
## Parses for/while ## Parses for/while
# Detect indentation # Detect indentation
let indent = detect_indent(value, index) let indent = detect_indent(value, index)
# Parse until an open brace `{` # Parse until an open brace `{`
var splitValue: string var splitValue: string
var read = value.parseUntil(splitValue, '{', index) var read = value.parseUntil(splitValue, '{', index)
result = parseExpr(splitValue & ":nil") result = parseExpr(splitValue & ":nil")
inc(index, read + 1) inc(index, read + 1)
# Parse through EOL # Parse through EOL
inc(index, value.parse_thru_eol(index)) inc(index, value.parse_thru_eol(index))
# Parse through { .. } # Parse through { .. }
read = value.parse_to_close(index, open='{', close='}', opened=1) read = value.parse_to_close(index, open='{', close='}', opened=1)
# Add parsed sub-expression into body # Add parsed sub-expression into body
var body = newStmtList() var body = newStmtList()
var stmtString = value.substring(index, read) var stmtString = value.substring(index, read)
trim_after_eol(stmtString) trim_after_eol(stmtString)
stmtString = reindent(stmtString, indent) stmtString = reindent(stmtString, indent)
parse_template(body, stmtString) parse_template(body, stmtString)
inc(index, read + 1) inc(index, read + 1)
# Insert body into result # Insert body into result
var stmtIndex = macros.high(result) var stmtIndex = result.len-1
result[stmtIndex] = body result[stmtIndex] = body
# Parse through EOL again # Parse through EOL again
inc(index, value.parse_thru_eol(index)) inc(index, value.parse_thru_eol(index))
proc parse_until_symbol(node: NimNode, value: string, index: var int): bool {.compiletime.} = proc parse_until_symbol(node: NimNode, value: string, index: var int): bool {.compiletime.} =
## Parses a string until a $ symbol is encountered, if ## Parses a string until a $ symbol is encountered, if
## two $$'s are encountered in a row, a split will happen ## two $$'s are encountered in a row, a split will happen
## removing one of the $'s from the resulting output ## removing one of the $'s from the resulting output
var splitValue: string var splitValue: string
var read = value.parseUntil(splitValue, '$', index) var read = value.parseUntil(splitValue, '$', index)
var insertionPoint = node.len var insertionPoint = node.len
inc(index, read + 1) inc(index, read + 1)
if index < value.len: if index < value.len:
case value[index] case value[index]
of '$': of '$':
# Check for duplicate `$`, meaning this is an escaped $ # Check for duplicate `$`, meaning this is an escaped $
node.add newCall("add", ident("result"), newStrLitNode("$")) node.add newCall("add", ident("result"), newStrLitNode("$"))
inc(index) inc(index)
of '(': of '(':
# Check for open `(`, which means parse as simple single-line expression. # Check for open `(`, which means parse as simple single-line expression.
trim_eol(splitValue) trim_eol(splitValue)
read = value.parse_to_close(index) + 1 read = value.parse_to_close(index) + 1
node.add newCall("add", ident("result"), node.add newCall("add", ident("result"),
newCall(bindSym"strip", parseExpr("$" & value.substring(index, read))) newCall(bindSym"strip", parseExpr("$" & value.substring(index, read)))
) )
inc(index, read) inc(index, read)
of '{': of '{':
# Check for open `{`, which means open statement list # Check for open `{`, which means open statement list
trim_eol(splitValue) trim_eol(splitValue)
for s in value.parse_stmt_list(index): for s in value.parse_stmt_list(index):
node.add parseExpr(s) node.add parseExpr(s)
else: else:
# Otherwise parse while valid `identChars` and make expression w/ $ # Otherwise parse while valid `identChars` and make expression w/ $
var identifier: string var identifier: string
read = value.parseWhile(identifier, identChars, index) read = value.parseWhile(identifier, identChars, index)
if identifier in ["for", "while"]: if identifier in ["for", "while"]:
## for/while means open simple statement ## for/while means open simple statement
trim_eol(splitValue) trim_eol(splitValue)
node.add value.parse_simple_statement(index) node.add value.parse_simple_statement(index)
elif identifier in ["if", "when", "case", "try"]: elif identifier in ["if", "when", "case", "try"]:
## if/when/case/try means complex statement ## if/when/case/try means complex statement
trim_eol(splitValue) trim_eol(splitValue)
node.add value.parse_complex_stmt(identifier, index) node.add value.parse_complex_stmt(identifier, index)
elif identifier.len > 0: elif identifier.len > 0:
## Treat as simple variable ## Treat as simple variable
node.add newCall("add", ident("result"), newCall("$", ident(identifier))) node.add newCall("add", ident("result"), newCall("$", ident(identifier)))
inc(index, read) inc(index, read)
result = true result = true
# Insert # Insert
if splitValue.len > 0: if splitValue.len > 0:
node.insert insertionPoint, newCall("add", ident("result"), newStrLitNode(splitValue)) node.insert insertionPoint, newCall("add", ident("result"), newStrLitNode(splitValue))
proc parse_template(node: NimNode, value: string) = proc parse_template(node: NimNode, value: string) =
## Parses through entire template, outputing valid ## Parses through entire template, outputing valid
## Nim code into the input `node` AST. ## Nim code into the input `node` AST.
var index = 0 var index = 0
while index < value.len and while index < value.len and
parse_until_symbol(node, value, index): nil parse_until_symbol(node, value, index): nil
macro tmpli*(body: expr): stmt = macro tmpli*(body: expr): stmt =
result = newStmtList() result = newStmtList()
result.add parseExpr("result = \"\"") result.add parseExpr("result = \"\"")
var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
else: body[1].strVal else: body[1].strVal
parse_template(result, reindent(value)) parse_template(result, reindent(value))
macro tmpl*(body: expr): stmt = macro tmpl*(body: expr): stmt =
result = newStmtList() result = newStmtList()
var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal var value = if body.kind in nnkStrLit..nnkTripleStrLit: body.strVal
else: body[1].strVal else: body[1].strVal
parse_template(result, reindent(value)) parse_template(result, reindent(value))
# Run tests # Run tests
when isMainModule: when isMainModule:
include otests include otests
echo "Success" echo "Success"