## Python tokenizer for Nimcheck.
##
## Tokenizes Python source code including:
## - Single/double quoted strings
## - Triple-quoted strings (""" ''')
## - f-strings with interpolation
## - Line continuation with backslash
## - Indentation tracking
import std/[strformat, sets, json, sequtils, unicode]
{.warning[UnusedImport]:off.}
import ../core/types, ../core/tokenizerbase
type
PythonTokenizer* = ref object of TokenizerBase
## Tokenizer for Python source code.
indentStack*: seq[int] # Track indentation levels
parenDepth*: int # Track parens for implicit continuation
# ---------------------------------------------------------------------------
# Python keywords
# ---------------------------------------------------------------------------
const
pythonKeywords* = @[
"False", "None", "True", "and", "as", "assert", "async", "await",
"break", "case", "class", "continue", "def", "del", "elif", "else", "except",
"finally", "for", "from", "global", "if", "import", "in", "is",
"lambda", "match", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield"
]
pythonKeywordSet* = pythonKeywords.toHashSet()
# ---------------------------------------------------------------------------
# Construction
# ---------------------------------------------------------------------------
proc newPythonTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): PythonTokenizer =
## Create a new Python tokenizer.
result = PythonTokenizer(
source: source,
sourceLen: source.len,
options: options,
tokens: @[],
pos: 0,
line: 1,
column: 1,
bracketStack: @[],
errors: @[],
eof: false
)
result.indentStack = @[0]
result.parenDepth = 0
# ---------------------------------------------------------------------------
# Indentation helpers
# ---------------------------------------------------------------------------
proc getIndentLevel*(line: string): int =
## Count leading spaces.
for c in line:
if c == ' ': result += 1
elif c == '\t': result += 8 # Standard tab = 8
else: break
# ---------------------------------------------------------------------------
# Tokenization
# ---------------------------------------------------------------------------
method tokenize*(self: PythonTokenizer) =
## Tokenize Python source code.
when defined(nimcheckDebug):
if self.options.debugMode:
debugEnter("PYTHON_TOKENIZER", "Starting Python tokenization")
var currentLine: string = ""
var atLineStart = true
while self.hasMore():
let startPos = self.currentPos()
let c = self.peek()
# Newline handling with indentation tracking
if c == '\n':
if atLineStart and currentLine.len == 0:
# Empty line, just emit newline
discard self.advance()
self.emitToken(tkNewline, "\n", startPos)
else:
# End of line - parse indentation of next line
discard self.advance()
self.emitToken(tkNewline, "\n", startPos)
atLineStart = true
currentLine = ""
# Read next line for indentation tracking (peek ahead)
var nextLineStart = self.pos
var indentCount = 0
while nextLineStart < self.sourceLen and self.source[nextLineStart] == ' ':
indentCount += 1
nextLineStart += 1
# Skip blank/comment lines
if nextLineStart < self.sourceLen and self.source[nextLineStart] != '\n' and self.source[nextLineStart] != '#':
if self.parenDepth == 0:
if indentCount > self.indentStack[^1]:
self.indentStack.add(indentCount)
self.emitToken(tkSpecial, "<indent>", startPos)
elif indentCount < self.indentStack[^1]:
while self.indentStack.len > 1 and indentCount < self.indentStack[^1]:
discard self.indentStack.pop()
self.emitToken(tkSpecial, "<dedent>", startPos)
if indentCount != self.indentStack[^1]:
self.recordError(esError, "Indentation mismatch", ErrUnexpectedIndent, startPos)
continue
atLineStart = false
# Whitespace (non-newline)
if c == ' ' or c == '\t' or c == '\r':
let wsStart = self.currentPos()
var ws = ""
while self.hasMore() and (self.peek() == ' ' or self.peek() == '\t' or self.peek() == '\r'):
ws.add(self.advance())
self.emitToken(tkWhitespace, ws, wsStart)
# Line comment
elif c == '#':
let comStart = self.currentPos()
discard self.advance()
var content = "#" & self.parseLineComment()
self.emitToken(tkComment, content, comStart)
# Triple-quoted strings
elif self.peekString(3) == "\"\"\"" or self.peekString(3) == "'''":
let quoteChar = if self.peek() == '"': '"' else: '\''
let strStart = self.currentPos()
discard self.advance()
discard self.advance()
discard self.advance()
var content = repeat($quoteChar, 3)
var foundClosing = false
while self.hasMore():
if self.peekString(3) == repeat($quoteChar, 3):
discard self.advance()
discard self.advance()
discard self.advance()
content.add(repeat($quoteChar, 3))
self.emitToken(tkMultilineString, content, strStart)
foundClosing = true
break
else:
content.add(self.advance())
if not foundClosing:
self.recordError(esError, "Unclosed triple-quoted string", ErrUnclosedString, strStart)
self.emitToken(tkMultilineString, content, strStart)
# f-strings
elif c == 'f' or c == 'F':
let peekStr = self.peekString(2)
if peekStr.len >= 2 and (peekStr[1] == '"' or peekStr[1] == '\''):
let fStart = self.currentPos()
discard self.advance() # f
let quote = self.advance() # " or '
var content = "f" & quote
var braceDepth = 0
var foundClosing = false
while self.hasMore():
let fc = self.advance()
if fc == '{' and braceDepth == 0:
braceDepth += 1
content.add(fc)
elif fc == '}' and braceDepth == 1:
braceDepth -= 1
content.add(fc)
elif fc == '\\':
content.add(fc)
if self.hasMore(): content.add(self.advance())
elif fc == quote and braceDepth == 0:
content.add(fc)
self.emitToken(tkString, content, fStart)
foundClosing = true
break
else:
content.add(fc)
if fc == '{': braceDepth += 1
elif fc == '}': braceDepth -= 1
if not foundClosing:
self.recordError(esError, "Unclosed f-string", ErrUnclosedString, fStart)
self.emitToken(tkString, content, fStart)
else:
# Regular identifier
var id = "f"
while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'):
id.add(self.advance())
self.emitToken(tkIdentifier, id, startPos)
# Regular string (single or double quote)
elif c == '"' or c == '\'':
let quote = c
let strStart = self.currentPos()
discard self.advance() # opening quote
var content = $quote
var foundClosing = false
while self.hasMore():
let sc = self.advance()
if sc == '\\':
content.add(sc)
if self.hasMore(): content.add(self.advance())
elif sc == quote:
content.add(quote)
self.emitToken(tkString, content, strStart)
foundClosing = true
break
elif sc == '\n':
self.recordError(esError, "Newline in string literal", ErrUnclosedString, strStart)
content.add('\n')
foundClosing = true
break
else:
content.add(sc)
if not foundClosing:
self.recordError(esError, "Unclosed string", ErrUnclosedString, strStart)
self.emitToken(tkString, content, strStart)
# Numbers
elif isDigit(c):
let numStart = self.currentPos()
var num = ""
# Check prefix
if c == '0' and self.hasMore():
let nxt = self.peek(1)
if nxt == 'x' or nxt == 'X':
num.add(self.advance())
num.add(self.advance())
while self.hasMore() and isHexDigit(self.peek()):
num.add(self.advance())
self.emitToken(tkNumber, num, numStart)
continue
elif nxt == 'b' or nxt == 'B':
num.add(self.advance())
num.add(self.advance())
while self.hasMore() and isBinaryDigit(self.peek()):
num.add(self.advance())
self.emitToken(tkNumber, num, numStart)
continue
elif nxt == 'o' or nxt == 'O':
num.add(self.advance())
num.add(self.advance())
while self.hasMore() and isOctalDigit(self.peek()):
num.add(self.advance())
self.emitToken(tkNumber, num, numStart)
continue
while self.hasMore() and (self.peek().isDigit() or self.peek() == '.' or self.peek() == 'e' or self.peek() == 'E' or self.peek() == 'j' or self.peek() == 'J'):
num.add(self.advance())
self.emitToken(tkNumber, num, numStart)
# Identifiers and keywords
elif isAlpha(c) or c == '_':
let idStart = self.currentPos()
var ident = ""
while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'):
ident.add(self.advance())
if pythonKeywordSet.contains(ident):
self.emitToken(tkKeyword, ident, idStart)
else:
self.emitToken(tkIdentifier, ident, idStart)
# Brackets
elif c == '(':
self.parenDepth += 1
self.emitBracketToken(kOpenParen, $self.advance(), startPos)
elif c == ')':
if self.parenDepth > 0: self.parenDepth -= 1
self.emitBracketToken(kCloseParen, $self.advance(), startPos)
if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '(':
discard self.bracketStack.pop()
elif c == '[':
self.parenDepth += 1
self.emitBracketToken(kOpenBracket, $self.advance(), startPos)
elif c == ']':
if self.parenDepth > 0: self.parenDepth -= 1
self.emitBracketToken(kCloseBracket, $self.advance(), startPos)
if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '[':
discard self.bracketStack.pop()
elif c == '{':
self.parenDepth += 1
self.emitBracketToken(kOpenBrace, $self.advance(), startPos)
elif c == '}':
if self.parenDepth > 0: self.parenDepth -= 1
self.emitBracketToken(kCloseBrace, $self.advance(), startPos)
if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '{':
discard self.bracketStack.pop()
# Operators
elif c in {'+', '-', '*', '/', '%', '^', '~', '|', '&', '<', '>', '='}:
let opStart = self.currentPos()
var op = ""
# Multi-char operators
while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '^', '~', '|', '&', '<', '>', '=', '!'}:
op.add(self.advance())
if op == "=" or op == "+=" or op == "-=" or op == "*=" or op == "/=" or op == "%=" or op == "//=" or op == "**=" or op == "&=" or op == "|=" or op == "^=" or op == "<<=" or op == ">>=":
self.emitToken(tkAssignment, op, opStart)
elif op == ":":
self.emitToken(tkPunctuation, op, opStart)
else:
self.emitToken(tkOperator, op, opStart)
# Punctuation
elif c in {'.', ',', ';', ':'}:
let pStart = self.currentPos()
if c == ':' and self.hasMore() and self.peek(1) == '=':
discard self.advance()
discard self.advance()
self.emitToken(tkAssignment, ":=", pStart)
else:
self.emitToken(tkPunctuation, $self.advance(), pStart)
# Backslash continuation
elif c == '\\':
let bsStart = self.currentPos()
discard self.advance()
if self.hasMore() and self.peek() == '\n':
discard self.advance()
self.emitToken(tkSpecial, "\\\n", bsStart)
else:
self.emitToken(tkOperator, "\\", bsStart)
# Decorator
elif c == '@':
let decStart = self.currentPos()
discard self.advance()
var decorator = "@"
while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_' or self.peek() == '.'):
decorator.add(self.advance())
self.emitToken(tkDirective, decorator, decStart)
# Unknown
else:
let uStart = self.currentPos()
self.emitToken(tkError, $self.advance(), uStart)
# Flush remaining indentation at EOF
# Trailing indent at EOF is valid Python - emit dedent tokens silently.
while self.indentStack.len > 1:
discard self.indentStack.pop()
self.emitToken(tkSpecial, "<dedent>", self.currentPos())
# End of file
self.emitToken(tkEndOfFile, "", self.currentPos())
when defined(nimcheckDebug):
if self.options.debugMode:
debugLeave("PYTHON_TOKENIZER", &"Tokenization complete: {self.tokens.len} tokens")