feat: bootstrap skeleton + lexer (Phase 0)

This commit is contained in:
2026-05-30 21:01:26 +03:00
commit 1b708ec755
13 changed files with 1684 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
import std/[os, strutils, terminal, strformat]
import lexer, manifest
type
ColorMode* = enum
cmAuto
cmOn
cmOff
GlobalOptions* = object
color*: ColorMode
quiet*: bool
verbose*: bool
proc printUsage*() =
echo """Bux Programming Language (bootstrap compiler)
Usage: bux [options] <command> [command-options]
Commands:
new <name> Create a new Bux package
init Initialize a Bux package in the current directory
build Build the current package
run Build and run the current package
check Type-check the current package
clean Remove build artifacts
help Show this help message
version Show version
Global options:
--color <auto|on|off> Control colored output (default: auto)
-q, --quiet Suppress non-error output
-v, --verbose Verbose output
"""
proc parseGlobalOptions(args: seq[string]): tuple[opts: GlobalOptions, rest: seq[string], ok: bool] =
result.opts = GlobalOptions(color: cmAuto, quiet: false, verbose: false)
result.rest = @[]
result.ok = true
var i = 0
while i < args.len:
let arg = args[i]
if arg == "--color":
if i + 1 >= args.len:
stderr.writeLine("error: --color requires an argument")
result.ok = false
return
inc i
case args[i].toLowerAscii()
of "auto": result.opts.color = cmAuto
of "on": result.opts.color = cmOn
of "off": result.opts.color = cmOff
else:
stderr.writeLine(&"error: unknown --color value '{args[i]}'")
result.ok = false
return
elif arg == "-q" or arg == "--quiet":
result.opts.quiet = true
elif arg == "-v" or arg == "--verbose":
result.opts.verbose = true
else:
result.rest.add(arg)
inc i
proc shouldUseColor(opts: GlobalOptions): bool =
case opts.color
of cmOn: true
of cmOff: false
of cmAuto: terminal.isatty(stdout)
proc printError(msg: string, useColor: bool) =
if useColor:
stdout.setForegroundColor(fgRed)
stdout.write("error: ")
stdout.resetAttributes()
stdout.writeLine(msg)
else:
stderr.writeLine("error: " & msg)
proc printInfo(msg: string, useColor: bool) =
if useColor:
stdout.setForegroundColor(fgCyan)
stdout.write("info: ")
stdout.resetAttributes()
stdout.writeLine(msg)
else:
echo("info: " & msg)
# ---------------------------------------------------------------------------
# Commands
# ---------------------------------------------------------------------------
proc cmdNew*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
if args.len < 1:
printError("'new' requires a package name", useColor)
return 1
let name = args[0]
let root = getCurrentDir() / name
if dirExists(root):
printError(&"directory '{name}' already exists", useColor)
return 1
createDir(root / "src")
writeFile(root / "bux.toml", &"""[Package]
Name = "{name}"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
""")
writeFile(root / "src" / "Main.bux", """import Std::Io::PrintLine;
func Main() -> int {
PrintLine(c8"Hello, Bux!");
return 0;
}
""")
if not opts.quiet:
printInfo(&"Created Bux package '{name}'", useColor)
return 0
proc cmdInit*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
if fileExists(root / "bux.toml"):
printError("bux.toml already exists", useColor)
return 1
let name = splitPath(root).tail
writeFile(root / "bux.toml", &"""[Package]
Name = "{name}"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
""")
if not dirExists(root / "src"):
createDir(root / "src")
if not opts.quiet:
printInfo(&"Initialized Bux package '{name}'", useColor)
return 0
proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let root = getCurrentDir()
let manifestPath = root / "bux.toml"
if not fileExists(manifestPath):
printError("no bux.toml found", useColor)
return 1
let man = loadManifest(manifestPath)
let srcDir = root / "src"
if not dirExists(srcDir):
printError("no src/ directory found", useColor)
return 1
var foundMain = false
for kind, path in walkDir(srcDir):
if kind == pcFile and path.endsWith(".bux"):
let source = readFile(path)
let res = tokenize(source, path)
if res.hasErrors:
printError(&"lex errors in {path}", useColor)
for d in res.diagnostics:
echo $d
return 1
if opts.verbose:
printInfo(&"lexed {path} ({res.tokens.len} tokens)", useColor)
if splitFile(path).name == "Main":
foundMain = true
if not foundMain:
printError("no Main.bux found in src/", useColor)
return 1
if not opts.quiet:
printInfo("check passed (lexer only)", useColor)
return 0
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
# For now, just type-check
let checkRes = cmdCheck(args, opts)
if checkRes != 0:
return checkRes
if not opts.quiet:
printInfo("build: nothing to do yet (no codegen)", useColor)
return 0
proc cmdRun*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
let buildRes = cmdBuild(args, opts)
if buildRes != 0:
return buildRes
printError("run: no executable produced yet (no codegen)", useColor)
return 1
proc cmdClean*(args: seq[string], opts: GlobalOptions): int =
let useColor = shouldUseColor(opts)
# Placeholder
if not opts.quiet:
printInfo("clean: nothing to do yet", useColor)
return 0
proc cmdVersion*(args: seq[string], opts: GlobalOptions): int =
echo "bux 0.1.0 (bootstrap)"
return 0
proc runCli*(args: seq[string]): int =
let (opts, rest, ok) = parseGlobalOptions(args)
if not ok:
return 1
if rest.len == 0:
printUsage()
return 0
let cmd = rest[0]
let cmdArgs = if rest.len > 1: rest[1..^1] else: @[]
case cmd
of "new": return cmdNew(cmdArgs, opts)
of "init": return cmdInit(cmdArgs, opts)
of "build": return cmdBuild(cmdArgs, opts)
of "run": return cmdRun(cmdArgs, opts)
of "check": return cmdCheck(cmdArgs, opts)
of "clean": return cmdClean(cmdArgs, opts)
of "version", "--version", "-v": return cmdVersion(cmdArgs, opts)
of "help", "--help", "-h":
printUsage()
return 0
else:
let useColor = shouldUseColor(opts)
printError(&"unknown command '{cmd}'", useColor)
return 1
+565
View File
@@ -0,0 +1,565 @@
import std/[strutils, strformat]
import token, source_location
type
LexerDiagnosticSeverity* = enum
ldsWarning
ldsError
LexerDiagnostic* = object
severity*: LexerDiagnosticSeverity
loc*: SourceLocation
message*: string
LexerResult* = object
tokens*: seq[Token]
diagnostics*: seq[LexerDiagnostic]
proc hasErrors*(res: LexerResult): bool =
for d in res.diagnostics:
if d.severity == ldsError:
return true
return false
proc `$`*(d: LexerDiagnostic): string =
let sev = if d.severity == ldsError: "error" else: "warning"
result = &"{sev}: {d.message} at {d.loc}"
type
Lexer* = object
source: string
sourceName: string
pos: int
line: uint32
col: uint32
tokens: seq[Token]
diagnostics: seq[LexerDiagnostic]
proc initLexer*(source, sourceName: string): Lexer =
result.source = source
result.sourceName = sourceName
result.pos = 0
result.line = 1
result.col = 1
proc isAtEnd(lex: Lexer): bool =
lex.pos >= lex.source.len
proc peek(lex: Lexer, ahead: int = 0): char =
let i = lex.pos + ahead
if i < lex.source.len:
return lex.source[i]
return '\0'
proc advance(lex: var Lexer): char =
result = lex.peek()
if not lex.isAtEnd():
inc lex.pos
if result == '\n':
inc lex.line
lex.col = 1
else:
inc lex.col
proc match(lex: var Lexer, expected: char): bool =
if lex.isAtEnd(): return false
if lex.peek() != expected: return false
discard lex.advance()
return true
proc matchStr(lex: var Lexer, s: string): bool =
for i, c in s:
if lex.peek(i) != c:
return false
for _ in s:
discard lex.advance()
return true
proc currentLocation(lex: Lexer): SourceLocation =
result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos))
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
let text = lex.source[startPos ..< lex.pos]
result = Token(kind: kind, text: text, loc: startLoc)
# ---------------------------------------------------------------------------
# Whitespace / comments
# ---------------------------------------------------------------------------
proc skipLineComment(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() != '\n':
discard lex.advance()
proc skipBlockComment(lex: var Lexer) =
let startLoc = lex.currentLocation()
var depth = 1
while not lex.isAtEnd() and depth > 0:
if lex.peek() == '/' and lex.peek(1) == '*':
discard lex.advance()
discard lex.advance()
inc depth
elif lex.peek() == '*' and lex.peek(1) == '/':
discard lex.advance()
discard lex.advance()
dec depth
else:
discard lex.advance()
if depth > 0:
lex.emitError(startLoc, "unterminated block comment")
proc skipWhitespace(lex: var Lexer) =
while not lex.isAtEnd():
let c = lex.peek()
if c in {' ', '\t', '\r'}:
discard lex.advance()
elif c == '/' and lex.peek(1) == '/':
lex.skipLineComment()
elif c == '/' and lex.peek(1) == '*':
discard lex.advance()
discard lex.advance()
lex.skipBlockComment()
else:
break
# ---------------------------------------------------------------------------
# Identifiers
# ---------------------------------------------------------------------------
proc isIdentStart(c: char): bool =
c in {'a'..'z', 'A'..'Z', '_'}
proc isIdentChar(c: char): bool =
c in {'a'..'z', 'A'..'Z', '0'..'9', '_'}
proc scanIdent(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
while not lex.isAtEnd() and isIdentChar(lex.peek()):
discard lex.advance()
let text = lex.source[startPos ..< lex.pos]
let kind = keywordKind(text)
result = Token(kind: kind, text: text, loc: startLoc)
# ---------------------------------------------------------------------------
# Numbers
# ---------------------------------------------------------------------------
proc scanIntSuffix(lex: var Lexer) =
# i8, i16, i32, i64, u8, u16, u32, u64, f32, f64
if lex.isAtEnd(): return
let c = lex.peek()
if c in {'i', 'u', 'f'}:
discard lex.advance()
while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
discard lex.advance()
proc scanHexDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'9', 'a'..'f', 'A'..'F'}:
discard lex.advance()
proc scanBinDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0', '1'}:
discard lex.advance()
proc scanOctDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'7'}:
discard lex.advance()
proc scanDecDigits(lex: var Lexer) =
while not lex.isAtEnd() and lex.peek() in {'0'..'9'}:
discard lex.advance()
proc scanNumber(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
var isFloat = false
if lex.peek() == '0' and lex.peek(1) in {'x', 'X', 'b', 'B', 'o', 'O'}:
discard lex.advance() # '0'
let prefix = lex.advance()
case prefix
of 'x', 'X': lex.scanHexDigits()
of 'b', 'B': lex.scanBinDigits()
of 'o', 'O': lex.scanOctDigits()
else: discard
lex.scanIntSuffix()
return lex.makeToken(tkIntLiteral, startLoc, startPos)
lex.scanDecDigits()
# Fractional part
if lex.peek() == '.' and lex.peek(1) in {'0'..'9'}:
isFloat = true
discard lex.advance() # '.'
lex.scanDecDigits()
# Exponent
if lex.peek() in {'e', 'E'}:
isFloat = true
discard lex.advance()
if lex.peek() in {'+', '-'}:
discard lex.advance()
if lex.peek() notin {'0'..'9'}:
lex.emitError(lex.currentLocation(), "expected digits in exponent")
else:
lex.scanDecDigits()
if isFloat:
# Optional f32/f64 suffix
if lex.peek() == 'f' and lex.peek(1) in {'3', '6'}:
discard lex.advance()
discard lex.advance()
result = lex.makeToken(tkFloatLiteral, startLoc, startPos)
else:
lex.scanIntSuffix()
result = lex.makeToken(tkIntLiteral, startLoc, startPos)
# ---------------------------------------------------------------------------
# Strings and chars
# ---------------------------------------------------------------------------
proc scanEscapeSequence(lex: var Lexer): string =
if lex.isAtEnd():
return ""
let c = lex.advance()
case c
of '\\': result = "\\"
of '"': result = "\""
of '\'': result = "\'"
of 'n': result = "\n"
of 'r': result = "\r"
of 't': result = "\t"
of '0': result = "\0"
of 'x':
var hexVal = ""
for _ in 0..<2:
if lex.isAtEnd() or lex.peek() notin {'0'..'9', 'a'..'f', 'A'..'F'}:
lex.emitError(lex.currentLocation(), "expected two hex digits after \\x")
break
hexVal.add(lex.advance())
if hexVal.len == 2:
try:
let code = parseHexInt(hexVal)
result = $chr(code)
except ValueError:
lex.emitError(lex.currentLocation(), &"invalid hex escape \\x{hexVal}")
result = ""
else:
result = ""
else:
lex.emitError(lex.currentLocation(), &"unknown escape sequence \\\\{c}")
result = $c
proc scanString(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
let startPos = lex.pos - prefixLen
# prefixLen characters before the opening quote were already consumed by caller
# but we need to handle the quote itself
if lex.peek() == '"':
discard lex.advance()
while not lex.isAtEnd() and lex.peek() != '"':
if lex.peek() == '\n':
lex.emitError(lex.currentLocation(), "unterminated string literal")
break
if lex.peek() == '\\':
discard lex.advance()
discard lex.scanEscapeSequence()
else:
discard lex.advance()
if lex.isAtEnd():
lex.emitError(startLoc, "unterminated string literal")
else:
discard lex.advance() # closing "
result = lex.makeToken(tkStringLiteral, startLoc, startPos)
proc scanChar(lex: var Lexer, startLoc: SourceLocation, prefixLen: int): Token =
let startPos = lex.pos - prefixLen
if lex.peek() == '\'':
discard lex.advance()
if lex.isAtEnd():
lex.emitError(startLoc, "unterminated char literal")
return lex.makeToken(tkCharLiteral, startLoc, startPos)
if lex.peek() == '\n':
lex.emitError(lex.currentLocation(), "newline in char literal")
elif lex.peek() == '\\':
discard lex.advance()
discard lex.scanEscapeSequence()
else:
discard lex.advance()
if lex.isAtEnd() or lex.peek() != '\'':
lex.emitError(lex.currentLocation(), "expected closing ' for char literal")
else:
discard lex.advance()
result = lex.makeToken(tkCharLiteral, startLoc, startPos)
# ---------------------------------------------------------------------------
# Symbols / operators
# ---------------------------------------------------------------------------
proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
let startPos = lex.pos
let c1 = lex.advance()
template check2(c2: char, kind2: TokenKind, kind1: TokenKind) =
if lex.peek() == c2:
discard lex.advance()
return lex.makeToken(kind2, startLoc, startPos)
else:
return lex.makeToken(kind1, startLoc, startPos)
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
if lex.peek() == c2:
discard lex.advance()
if lex.peek() == c3:
discard lex.advance()
return lex.makeToken(kind3, startLoc, startPos)
return lex.makeToken(kind2, startLoc, startPos)
else:
return lex.makeToken(kind1, startLoc, startPos)
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
check2(c2, kind2, kind1)
case c1
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
of '{': return lex.makeToken(tkLBrace, startLoc, startPos)
of '}': return lex.makeToken(tkRBrace, startLoc, startPos)
of '[': return lex.makeToken(tkLBracket, startLoc, startPos)
of ']': return lex.makeToken(tkRBracket, startLoc, startPos)
of ',': return lex.makeToken(tkComma, startLoc, startPos)
of ';': return lex.makeToken(tkSemicolon, startLoc, startPos)
of '@': return lex.makeToken(tkAt, startLoc, startPos)
of '?': return lex.makeToken(tkQuestion, startLoc, startPos)
of '~': return lex.makeToken(tkTilde, startLoc, startPos)
of ':': check2(':', tkColonColon, tkColon)
of '.':
if lex.peek() == '.' and lex.peek(1) == '.':
discard lex.advance()
discard lex.advance()
return lex.makeToken(tkDotDotDot, startLoc, startPos)
elif lex.peek() == '.' and lex.peek(1) == '=':
discard lex.advance()
discard lex.advance()
return lex.makeToken(tkDotDotEqual, startLoc, startPos)
elif lex.peek() == '.':
discard lex.advance()
return lex.makeToken(tkDotDot, startLoc, startPos)
else:
return lex.makeToken(tkDot, startLoc, startPos)
of '-':
if lex.peek() == '>':
discard lex.advance()
return lex.makeToken(tkArrow, startLoc, startPos)
elif lex.peek() == '-':
discard lex.advance()
return lex.makeToken(tkMinusMinus, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkMinusAssign, startLoc, startPos)
else:
return lex.makeToken(tkMinus, startLoc, startPos)
of '+':
if lex.peek() == '+':
discard lex.advance()
return lex.makeToken(tkPlusPlus, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPlusAssign, startLoc, startPos)
else:
return lex.makeToken(tkPlus, startLoc, startPos)
of '*':
if lex.peek() == '*':
discard lex.advance()
return lex.makeToken(tkStarStar, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkStarAssign, startLoc, startPos)
else:
return lex.makeToken(tkStar, startLoc, startPos)
of '/':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkSlashAssign, startLoc, startPos)
else:
return lex.makeToken(tkSlash, startLoc, startPos)
of '%':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPercentAssign, startLoc, startPos)
else:
return lex.makeToken(tkPercent, startLoc, startPos)
of '=':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkEq, startLoc, startPos)
elif lex.peek() == '>':
discard lex.advance()
return lex.makeToken(tkFatArrow, startLoc, startPos)
else:
return lex.makeToken(tkAssign, startLoc, startPos)
of '!':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkNe, startLoc, startPos)
else:
return lex.makeToken(tkBang, startLoc, startPos)
of '<':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkLe, startLoc, startPos)
elif lex.peek() == '<':
discard lex.advance()
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkShlAssign, startLoc, startPos)
return lex.makeToken(tkShl, startLoc, startPos)
else:
return lex.makeToken(tkLt, startLoc, startPos)
of '>':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkGe, startLoc, startPos)
elif lex.peek() == '>':
discard lex.advance()
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkShrAssign, startLoc, startPos)
return lex.makeToken(tkShr, startLoc, startPos)
else:
return lex.makeToken(tkGt, startLoc, startPos)
of '&':
if lex.peek() == '&':
discard lex.advance()
return lex.makeToken(tkAmpAmp, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkAmpAssign, startLoc, startPos)
else:
return lex.makeToken(tkAmp, startLoc, startPos)
of '|':
if lex.peek() == '|':
discard lex.advance()
return lex.makeToken(tkPipePipe, startLoc, startPos)
elif lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkPipeAssign, startLoc, startPos)
else:
return lex.makeToken(tkPipe, startLoc, startPos)
of '^':
if lex.peek() == '=':
discard lex.advance()
return lex.makeToken(tkCaretAssign, startLoc, startPos)
else:
return lex.makeToken(tkCaret, startLoc, startPos)
of '#':
# Check for intrinsics: #line, #column, #file, #function, #date, #time, #module
let afterHash = lex.peek()
if afterHash == 'l' and lex.matchStr("line"):
return lex.makeToken(tkHashLine, startLoc, startPos)
elif afterHash == 'c' and lex.matchStr("column"):
return lex.makeToken(tkHashColumn, startLoc, startPos)
elif afterHash == 'f' and lex.peek(1) == 'i' and lex.peek(2) == 'l' and lex.peek(3) == 'e':
discard lex.matchStr("file")
return lex.makeToken(tkHashFile, startLoc, startPos)
elif afterHash == 'f' and lex.peek(1) == 'u' and lex.peek(2) == 'n':
discard lex.matchStr("function")
return lex.makeToken(tkHashFunction, startLoc, startPos)
elif afterHash == 'd' and lex.matchStr("date"):
return lex.makeToken(tkHashDate, startLoc, startPos)
elif afterHash == 't' and lex.matchStr("time"):
return lex.makeToken(tkHashTime, startLoc, startPos)
elif afterHash == 'm' and lex.matchStr("module"):
return lex.makeToken(tkHashModule, startLoc, startPos)
else:
return lex.makeToken(tkHash, startLoc, startPos)
else:
lex.emitError(startLoc, &"unexpected character '{c1}'")
return lex.makeToken(tkUnknown, startLoc, startPos)
# ---------------------------------------------------------------------------
# Main scanning loop
# ---------------------------------------------------------------------------
proc nextToken(lex: var Lexer): Token =
lex.skipWhitespace()
let startLoc = lex.currentLocation()
let startPos = lex.pos
if lex.isAtEnd():
return Token(kind: tkEndOfFile, text: "", loc: startLoc)
let c = lex.peek()
if c == '\n':
discard lex.advance()
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
# String prefixes: c8" c16" c32" — must come before ident check
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
let d = lex.peek(1)
if d == '8' and lex.peek(2) == '"':
discard lex.advance() # c
discard lex.advance() # 8
return lex.scanString(startLoc, 2)
elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '"':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanString(startLoc, 3)
elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '"':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanString(startLoc, 3)
if c == '"':
return lex.scanString(startLoc, 0)
# Char prefixes: c8' c16' c32' — must come before ident check
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
let d = lex.peek(1)
if d == '8' and lex.peek(2) == '\'':
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 2)
elif d == '1' and lex.peek(2) == '6' and lex.peek(3) == '\'':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 3)
elif d == '3' and lex.peek(2) == '2' and lex.peek(3) == '\'':
discard lex.advance()
discard lex.advance()
discard lex.advance()
return lex.scanChar(startLoc, 3)
if c == '\'':
return lex.scanChar(startLoc, 0)
if isIdentStart(c):
return lex.scanIdent(startLoc)
if c in {'0'..'9'}:
return lex.scanNumber(startLoc)
return lex.scanSymbol(startLoc)
proc tokenize*(lex: var Lexer): LexerResult =
while true:
let tok = lex.nextToken()
lex.tokens.add(tok)
if tok.kind == tkEndOfFile:
break
result = LexerResult(tokens: lex.tokens, diagnostics: lex.diagnostics)
proc tokenize*(source, sourceName: string): LexerResult =
var lex = initLexer(source, sourceName)
result = lex.tokenize()
proc dumpTokens*(res: LexerResult): string =
for tok in res.tokens:
result.add($tok & "\n")
+6
View File
@@ -0,0 +1,6 @@
import std/os
import cli
when isMainModule:
let args = commandLineParams()
quit(runCli(args))
+79
View File
@@ -0,0 +1,79 @@
import std/[strutils, os, tables]
type
PackageType* = enum
ptExecutable
ptSharedLibrary
ptStaticLibrary
ptSource
Manifest* = object
name*: string
version*: string
pkgType*: PackageType
output*: string ## from [Build] Output
dependencies*: OrderedTableRef[string, string]
type
TomlValueKind = enum
tvkString, tvkTable
TomlValue = object
case kind*: TomlValueKind
of tvkString:
strVal*: string
of tvkTable:
tableVal*: OrderedTableRef[string, TomlValue]
proc parseSimpleToml(path: string): OrderedTableRef[string, TomlValue] =
result = newOrderedTable[string, TomlValue]()
if not fileExists(path):
return result
let content = readFile(path)
var currentTable = ""
for rawLine in content.splitLines():
let line = rawLine.strip()
if line.len == 0 or line.startsWith("#"):
continue
if line.startsWith("[") and line.endsWith("]"):
currentTable = line[1 ..< ^1]
if not result.hasKey(currentTable):
result[currentTable] = TomlValue(kind: tvkTable, tableVal: newOrderedTable[string, TomlValue]())
else:
let eqIdx = line.find('=')
if eqIdx >= 0:
let key = line[0 ..< eqIdx].strip()
var val = line[eqIdx + 1 .. ^1].strip()
# Remove surrounding quotes
if val.len >= 2 and val[0] == '"' and val[^1] == '"':
val = val[1 ..< ^1]
if currentTable != "" and result.hasKey(currentTable):
result[currentTable].tableVal[key] = TomlValue(kind: tvkString, strVal: val)
else:
result[key] = TomlValue(kind: tvkString, strVal: val)
proc loadManifest*(path: string): Manifest =
let data = parseSimpleToml(path)
result.name = ""
result.version = "0.1.0"
result.pkgType = ptExecutable
result.output = "Bin"
result.dependencies = newOrderedTable[string, string]()
if data.hasKey("Package"):
let pkg = data["Package"].tableVal
if pkg.hasKey("Name"):
result.name = pkg["Name"].strVal
if pkg.hasKey("Version"):
result.version = pkg["Version"].strVal
if pkg.hasKey("Type"):
case pkg["Type"].strVal.toLowerAscii()
of "bin", "executable": result.pkgType = ptExecutable
of "shared", "sharedlibrary", "dll", "so", "dylib": result.pkgType = ptSharedLibrary
of "static", "staticlibrary", "lib", "a": result.pkgType = ptStaticLibrary
of "source": result.pkgType = ptSource
else: result.pkgType = ptExecutable
if data.hasKey("Build"):
let bld = data["Build"].tableVal
if bld.hasKey("Output"):
result.output = bld["Output"].strVal
+8
View File
@@ -0,0 +1,8 @@
type
SourceLocation* = object
line*: uint32 ## 1-based
column*: uint32 ## 1-based (UTF-8 byte offset in line)
offset*: uint32 ## byte offset from start of file
proc `$`*(loc: SourceLocation): string =
$loc.line & ":" & $loc.column
+300
View File
@@ -0,0 +1,300 @@
import source_location
type
TokenKind* = enum
##Literals
tkIntLiteral # 42 0xFF 0b1010 0o77
tkFloatLiteral # 3.14 1.0e-9
tkStringLiteral # "hello" c8"hello" c16"hello" c32"hello"
tkCharLiteral # 'A' c8'A' c16'A' c32'A'
tkBoolLiteral # true false
##Identifiers
tkIdent # foo Bar _x
##Control flow keywords
tkIf # if
tkElse # else
tkWhile # while
tkDo # do
tkLoop # loop
tkFor # for
tkIn # in
tkBreak # break
tkContinue # continue
tkReturn # return
tkMatch # match
##Declaration keywords
tkFunc # func
tkLet # let
tkVar # var
tkConst # const
tkType # type
tkStruct # struct
tkEnum # enum
tkUnion # union
tkInterface # interface
tkExtend # extend
tkModule # module
tkImport # import
tkPub # pub
tkExtern # extern
##Other keywords
tkAs # as
tkIs # is
tkNull # null
tkSelf # self
tkSuper # super
##Punctuation
tkLParen # (
tkRParen # )
tkLBrace # {
tkRBrace # }
tkLBracket # [
tkRBracket # ]
tkComma # ,
tkSemicolon # ;
tkColon # :
tkColonColon # ::
tkDot # .
tkDotDot # ..
tkDotDotDot # ...
tkDotDotEqual # ..=
tkArrow # ->
tkFatArrow # =>
tkAt # @
tkHash # #
tkQuestion # ?
##Arithmetic operators
tkPlus # +
tkMinus # -
tkStar # *
tkSlash # /
tkPercent # %
tkStarStar # **
tkPlusPlus # ++
tkMinusMinus # --
##Bitwise operators
tkAmp # &
tkPipe # |
tkCaret # ^
tkTilde # ~
tkShl # <<
tkShr # >>
##Logical operators
tkAmpAmp # &&
tkPipePipe # ||
tkBang # !
##Comparison operators
tkEq # ==
tkNe # !=
tkLt # <
tkLe # <=
tkGt # >
tkGe # >=
##Assignment operators
tkAssign # =
tkPlusAssign # +=
tkMinusAssign # -=
tkStarAssign # *=
tkSlashAssign # /=
tkPercentAssign # %=
tkAmpAssign # &=
tkPipeAssign # |=
tkCaretAssign # ^=
tkShlAssign # <<=
tkShrAssign # >>=
##Compile-time intrinsics
tkHashLine # #line
tkHashColumn # #column
tkHashFile # #file
tkHashFunction # #function
tkHashDate # #date
tkHashTime # #time
tkHashModule # #module
##Special
tkNewLine # significant newline (if grammar uses them)
tkEndOfFile # end of file
tkUnknown # unrecognized character
Token* = object
kind*: TokenKind
text*: string # original source spelling
loc*: SourceLocation
proc isKeyword*(kind: TokenKind): bool =
case kind
of tkIf, tkElse, tkWhile, tkDo, tkLoop, tkFor, tkIn,
tkBreak, tkContinue, tkReturn, tkMatch,
tkFunc, tkLet, tkVar, tkConst, tkType, tkStruct, tkEnum,
tkUnion, tkInterface, tkExtend, tkModule, tkImport,
tkPub, tkExtern, tkAs, tkIs, tkNull, tkSelf, tkSuper:
true
else:
false
proc isLiteral*(kind: TokenKind): bool =
case kind
of tkIntLiteral, tkFloatLiteral, tkStringLiteral, tkCharLiteral, tkBoolLiteral:
true
else:
false
proc isOperator*(kind: TokenKind): bool =
case kind
of tkPlus..tkShrAssign:
true
else:
false
proc isEof*(kind: TokenKind): bool = kind == tkEndOfFile
proc keywordKind*(text: string): TokenKind =
case text
of "func": tkFunc
of "let": tkLet
of "var": tkVar
of "const": tkConst
of "type": tkType
of "struct": tkStruct
of "enum": tkEnum
of "union": tkUnion
of "interface": tkInterface
of "extend": tkExtend
of "module": tkModule
of "import": tkImport
of "pub": tkPub
of "extern": tkExtern
of "if": tkIf
of "else": tkElse
of "while": tkWhile
of "do": tkDo
of "loop": tkLoop
of "for": tkFor
of "in": tkIn
of "break": tkBreak
of "continue": tkContinue
of "return": tkReturn
of "match": tkMatch
of "as": tkAs
of "is": tkIs
of "null": tkNull
of "self": tkSelf
of "super": tkSuper
of "true", "false": tkBoolLiteral
else: tkIdent
proc tokenKindName*(kind: TokenKind): string =
case kind
of tkIntLiteral: "integer literal"
of tkFloatLiteral: "float literal"
of tkStringLiteral: "string literal"
of tkCharLiteral: "char literal"
of tkBoolLiteral: "boolean literal"
of tkIdent: "identifier"
of tkIf: "'if'"
of tkElse: "'else'"
of tkWhile: "'while'"
of tkDo: "'do'"
of tkLoop: "'loop'"
of tkFor: "'for'"
of tkIn: "'in'"
of tkBreak: "'break'"
of tkContinue: "'continue'"
of tkReturn: "'return'"
of tkMatch: "'match'"
of tkFunc: "'func'"
of tkLet: "'let'"
of tkVar: "'var'"
of tkConst: "'const'"
of tkType: "'type'"
of tkStruct: "'struct'"
of tkEnum: "'enum'"
of tkUnion: "'union'"
of tkInterface: "'interface'"
of tkExtend: "'extend'"
of tkModule: "'module'"
of tkImport: "'import'"
of tkPub: "'pub'"
of tkExtern: "'extern'"
of tkAs: "'as'"
of tkIs: "'is'"
of tkNull: "'null'"
of tkSelf: "'self'"
of tkSuper: "'super'"
of tkLParen: "'('"
of tkRParen: "')'"
of tkLBrace: "'{'"
of tkRBrace: "'}'"
of tkLBracket: "'['"
of tkRBracket: "']'"
of tkComma: "','"
of tkSemicolon: "';'"
of tkColon: "':'"
of tkColonColon: "'::'"
of tkDot: "'.'"
of tkDotDot: "'..'"
of tkDotDotDot: "'...'"
of tkDotDotEqual: "'..='"
of tkArrow: "'->'"
of tkFatArrow: "'=>'"
of tkAt: "'@'"
of tkHash: "'#'"
of tkQuestion: "'?'"
of tkPlus: "'+'"
of tkMinus: "'-'"
of tkStar: "'*'"
of tkSlash: "'/'"
of tkPercent: "'%'"
of tkStarStar: "'**'"
of tkPlusPlus: "'++'"
of tkMinusMinus: "'--'"
of tkAmp: "'&'"
of tkPipe: "'|'"
of tkCaret: "'^'"
of tkTilde: "'~'"
of tkShl: "'<<'"
of tkShr: "'>>'"
of tkAmpAmp: "'&&'"
of tkPipePipe: "'||'"
of tkBang: "'!'"
of tkEq: "'=='"
of tkNe: "'!='"
of tkLt: "'<'"
of tkLe: "'<='"
of tkGt: "'>'"
of tkGe: "'>='"
of tkAssign: "'='"
of tkPlusAssign: "'+='"
of tkMinusAssign: "'-='"
of tkStarAssign: "'*='"
of tkSlashAssign: "'/='"
of tkPercentAssign: "'%='"
of tkAmpAssign: "'&='"
of tkPipeAssign: "'|='"
of tkCaretAssign: "'^='"
of tkShlAssign: "'<<='"
of tkShrAssign: "'>>='"
of tkHashLine: "'#line'"
of tkHashColumn: "'#column'"
of tkHashFile: "'#file'"
of tkHashFunction: "'#function'"
of tkHashDate: "'#date'"
of tkHashTime: "'#time'"
of tkHashModule: "'#module'"
of tkNewLine: "newline"
of tkEndOfFile: "end of file"
of tkUnknown: "unknown token"
proc `$`*(tok: Token): string =
result = tokenKindName(tok.kind) & " '" & tok.text & "' @ " & $tok.loc