mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
- "x is \(x)", "\(speed, 2)" (two digits after the point), also in """ text
- test "name" { ... } blocks; assert(cond) and assert(cond, why); finch test runs each on its own
- the program's object and its C files (link "x.c") are kept in the cache until they or their headers change
- runtime errors show the calls that led there (finch run, finch test, -g, -O0, --trace)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GzNP6r8dg6WDxUVZy8wjL
470 lines
14 KiB
Text
470 lines
14 KiB
Text
// The Finch lexer, written in Finch.
|
|
// A token's kind is a word: "ident", "int", "float", "string", "char", "eof",
|
|
// or the keyword / operator itself ("fn", "{", "+=", ...).
|
|
|
|
struct Token {
|
|
str kind
|
|
str text
|
|
int line
|
|
int col
|
|
bool nl // a newline came before this token
|
|
}
|
|
|
|
fn isAlpha(char c) -> bool {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
|
}
|
|
|
|
fn isDigit(char c) -> bool {
|
|
return c >= '0' && c <= '9'
|
|
}
|
|
|
|
fn isHex(char c) -> bool {
|
|
return isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
|
}
|
|
|
|
fn isKeyword(str w) -> bool {
|
|
keywords := ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false",
|
|
"null", "import", "link", "struct", "defer", "or", "try", "enum", "match", "export", "const"]
|
|
return keywords.contains(w)
|
|
}
|
|
|
|
struct Lexer {
|
|
str src
|
|
str path
|
|
int i
|
|
int line
|
|
int col
|
|
bool nl
|
|
}
|
|
|
|
fn fail(ptr[Lexer] lx, int line, int col, str msg) {
|
|
print(lx.path + ":" + str(line) + ":" + str(col) + ": error: " + msg)
|
|
exit(1)
|
|
}
|
|
|
|
fn peek(ptr[Lexer] lx, int off) -> char {
|
|
if lx.i + off < lx.src.len {
|
|
return lx.src[lx.i + off]
|
|
}
|
|
return char(0)
|
|
}
|
|
|
|
fn advance(ptr[Lexer] lx) {
|
|
c := lx.src[lx.i]
|
|
if c == '\n' {
|
|
lx.line += 1
|
|
lx.col = 1
|
|
lx.nl = true
|
|
} else if int(c) & 0xC0 != 0x80 {
|
|
lx.col += 1
|
|
}
|
|
lx.i += 1
|
|
}
|
|
|
|
// operators, longest first
|
|
fn operator(ptr[Lexer] lx) -> str {
|
|
twos := ["->", "<<", ">>", "<=", ">=", "==", "!=", "&&", "||", "..", ":=", "+=", "-=", "*=", "/=", "%="]
|
|
if lx.i + 1 < lx.src.len {
|
|
two := lx.src.sub(lx.i, lx.i + 2)
|
|
if twos.contains(two) {
|
|
return two
|
|
}
|
|
}
|
|
ones := "(){}[],.:+-*/%&|^~<>=!"
|
|
c := lx.src.sub(lx.i, lx.i + 1)
|
|
if ones.contains(c) {
|
|
return c
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// """ text over several lines """: the line break after the opening quotes is dropped, and so is
|
|
// the indentation of the closing quotes, from every line (the text can be indented with the code).
|
|
// Gives the text as written (escapes and \(...) still in it); cook() finishes it.
|
|
fn longString(ptr[Lexer] lx, Token t) -> str {
|
|
for k in 0..3 {
|
|
advance(lx)
|
|
}
|
|
[]str parts
|
|
while !(peek(lx, 0) == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"') {
|
|
if lx.i >= lx.src.len {
|
|
fail(lx, t.line, t.col, "text is never closed (missing \"\"\")")
|
|
}
|
|
ch := peek(lx, 0)
|
|
if ch == '\\' && peek(lx, 1) == '(' {
|
|
start := lx.i
|
|
advance(lx)
|
|
advance(lx)
|
|
skipEmbedded(lx, t)
|
|
parts.push(lx.src.sub(start, lx.i))
|
|
continue
|
|
}
|
|
advance(lx)
|
|
if ch == '\r' {
|
|
continue
|
|
}
|
|
parts.push(str(ch))
|
|
if ch == '\\' && lx.i < lx.src.len {
|
|
parts.push(str(peek(lx, 0))) // \" doesn't end the text
|
|
advance(lx)
|
|
}
|
|
}
|
|
for k in 0..3 {
|
|
advance(lx)
|
|
}
|
|
lx.nl = false // the line breaks inside were text, not the end of a statement
|
|
body := parts.join("")
|
|
if body.starts_with("\n") {
|
|
body = body.sub(1, body.len)
|
|
}
|
|
last := body.split("\n")
|
|
indent := last[last.len - 1]
|
|
if last.len > 1 && indent.trim().len == 0 {
|
|
// the closing quotes are on their own line: their indentation goes from every line
|
|
[]str lines
|
|
for i in 0..last.len - 1 {
|
|
line := last[i]
|
|
if line.starts_with(indent) {
|
|
line = line.sub(indent.len, line.len)
|
|
} else if line.trim().len > 0 {
|
|
fail(lx, t.line, t.col, "a line of this text is indented less than the closing \"\"\"")
|
|
} else {
|
|
line = ""
|
|
}
|
|
lines.push(line)
|
|
}
|
|
return lines.join("\n")
|
|
}
|
|
return body
|
|
}
|
|
|
|
// "text" as written, up to the closing quote (which is skipped); escapes and \(...) are still in it
|
|
fn rawString(ptr[Lexer] lx, Token t) -> str {
|
|
advance(lx)
|
|
start := lx.i
|
|
while peek(lx, 0) != '"' {
|
|
if lx.i >= lx.src.len || peek(lx, 0) == '\n' {
|
|
fail(lx, t.line, t.col, "string is never closed (missing \")")
|
|
}
|
|
if peek(lx, 0) == '\\' && peek(lx, 1) == '(' {
|
|
advance(lx)
|
|
advance(lx)
|
|
skipEmbedded(lx, t)
|
|
continue
|
|
}
|
|
if peek(lx, 0) == '\\' {
|
|
advance(lx) // the escaped character can't end the string
|
|
}
|
|
advance(lx)
|
|
}
|
|
raw := lx.src.sub(start, lx.i)
|
|
advance(lx)
|
|
return raw
|
|
}
|
|
|
|
// past the code of a \( ... ), up to and including its )
|
|
fn skipEmbedded(ptr[Lexer] lx, Token t) {
|
|
depth := 1
|
|
while depth > 0 {
|
|
c := peek(lx, 0)
|
|
if lx.i >= lx.src.len || c == '\n' {
|
|
fail(lx, t.line, t.col, "a \\( in this text is never closed (missing ')')")
|
|
}
|
|
if c == '(' {
|
|
depth += 1
|
|
} else if c == ')' {
|
|
depth -= 1
|
|
} else if c == '"' || c == '\'' {
|
|
advance(lx)
|
|
while peek(lx, 0) != c {
|
|
if lx.i >= lx.src.len || peek(lx, 0) == '\n' {
|
|
fail(lx, t.line, t.col, "a text inside \\( ... ) is never closed")
|
|
}
|
|
if peek(lx, 0) == '\\' {
|
|
advance(lx)
|
|
}
|
|
advance(lx)
|
|
}
|
|
}
|
|
advance(lx)
|
|
}
|
|
}
|
|
|
|
fn punct(str k, Token at) -> Token {
|
|
return Token(kind: k, text: k, line: at.line, col: at.col, nl: false)
|
|
}
|
|
|
|
// The tokens of a text: one string, or for "x is \(x)" the tokens of ("x is " + str(x) + "").
|
|
// \(v, 2) is str(v, 2): a number with two digits after the point.
|
|
fn cook(ptr[Lexer] lx, str raw, Token t) -> []Token {
|
|
[]Token toks
|
|
[]str parts
|
|
pieces := 0
|
|
i := 0
|
|
while i < raw.len {
|
|
ch := raw[i]
|
|
if ch == '\\' && i + 1 < raw.len && raw[i + 1] == '(' {
|
|
j := i + 2
|
|
depth := 1
|
|
while depth > 0 {
|
|
c := raw[j]
|
|
if c == '(' {
|
|
depth += 1
|
|
} else if c == ')' {
|
|
depth -= 1
|
|
} else if c == '"' || c == '\'' {
|
|
j += 1
|
|
while raw[j] != c {
|
|
if raw[j] == '\\' {
|
|
j += 1
|
|
}
|
|
j += 1
|
|
}
|
|
}
|
|
j += 1
|
|
}
|
|
code := raw.sub(i + 2, j - 1)
|
|
if code.trim().len == 0 {
|
|
fail(lx, t.line, t.col, "\\( ) needs a value inside, like \\(x)")
|
|
}
|
|
if pieces == 0 {
|
|
open := punct("(", t)
|
|
open.nl = t.nl
|
|
toks.push(open)
|
|
}
|
|
toks.push(Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false))
|
|
[]str none
|
|
parts = none
|
|
toks.push(punct("+", t))
|
|
toks.push(Token(kind: "ident", text: "str", line: t.line, col: t.col, nl: false))
|
|
toks.push(punct("(", t))
|
|
inner := lexFrom(code, lx.path, t.line, t.col + 3 + i)
|
|
for k in 0..inner.len - 1 {
|
|
tk := inner[k]
|
|
tk.nl = false
|
|
toks.push(tk)
|
|
}
|
|
toks.push(punct(")", t))
|
|
toks.push(punct("+", t))
|
|
pieces += 1
|
|
i = j
|
|
continue
|
|
}
|
|
if ch == '\\' {
|
|
i += 1
|
|
ch = escapeAt(lx, raw[i], t.line, t.col)
|
|
}
|
|
parts.push(str(ch))
|
|
i += 1
|
|
}
|
|
last := Token(kind: "string", text: parts.join(""), line: t.line, col: t.col, nl: false)
|
|
if pieces == 0 {
|
|
last.nl = t.nl
|
|
toks.push(last)
|
|
return toks
|
|
}
|
|
toks.push(last)
|
|
toks.push(punct(")", t))
|
|
return toks
|
|
}
|
|
|
|
fn escapeAt(ptr[Lexer] lx, char e, int line, int col) -> char {
|
|
if e == 'n' {
|
|
return '\n'
|
|
}
|
|
if e == 't' {
|
|
return '\t'
|
|
}
|
|
if e == 'r' {
|
|
return '\r'
|
|
}
|
|
if e == '0' {
|
|
return char(0)
|
|
}
|
|
if e == '\\' || e == '"' || e == '\'' {
|
|
return e
|
|
}
|
|
fail(lx, line, col, "unknown escape \\" + str(e) + " in this text (\\n \\t \\r \\0 \\\\ \\\" \\' and \\(value))")
|
|
return e
|
|
}
|
|
|
|
fn escape(ptr[Lexer] lx, char e) -> char {
|
|
if e == 'n' {
|
|
return '\n'
|
|
}
|
|
if e == 't' {
|
|
return '\t'
|
|
}
|
|
if e == 'r' {
|
|
return '\r'
|
|
}
|
|
if e == '0' {
|
|
return char(0)
|
|
}
|
|
if e == '\\' || e == '"' || e == '\'' {
|
|
return e
|
|
}
|
|
fail(lx, lx.line, lx.col - 1, "unknown escape \\" + str(e))
|
|
return e
|
|
}
|
|
|
|
fn lex(str src, str path) -> []Token {
|
|
return lexFrom(src, path, 1, 1)
|
|
}
|
|
|
|
// `src` starts at this line and column of the file (the code inside a "\(...)")
|
|
fn lexFrom(str src, str path, int firstLine, int firstCol) -> []Token {
|
|
lexer := Lexer(src: src, path: path, i: 0, line: firstLine, col: firstCol, nl: true)
|
|
lx := addr(lexer)
|
|
[]Token out
|
|
while lx.i < lx.src.len {
|
|
c := lx.src[lx.i]
|
|
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
|
|
advance(lx)
|
|
continue
|
|
}
|
|
if c == '/' && peek(lx, 1) == '/' {
|
|
while lx.i < lx.src.len && lx.src[lx.i] != '\n' {
|
|
advance(lx)
|
|
}
|
|
continue
|
|
}
|
|
if c == '/' && peek(lx, 1) == '*' {
|
|
line := lx.line
|
|
col := lx.col
|
|
advance(lx)
|
|
advance(lx)
|
|
while lx.i < lx.src.len && !(lx.src[lx.i] == '*' && peek(lx, 1) == '/') {
|
|
advance(lx)
|
|
}
|
|
if lx.i >= lx.src.len {
|
|
fail(lx, line, col, "comment is never closed (missing */)")
|
|
}
|
|
advance(lx)
|
|
advance(lx)
|
|
continue
|
|
}
|
|
|
|
t := Token(kind: "", text: "", line: lx.line, col: lx.col, nl: lx.nl)
|
|
lx.nl = false
|
|
start := lx.i
|
|
if isAlpha(c) {
|
|
while lx.i < lx.src.len && (isAlpha(lx.src[lx.i]) || isDigit(lx.src[lx.i])) {
|
|
advance(lx)
|
|
}
|
|
t.text = lx.src.sub(start, lx.i)
|
|
t.kind = "ident"
|
|
if isKeyword(t.text) {
|
|
t.kind = t.text
|
|
}
|
|
} else if isDigit(c) {
|
|
t.kind = "int"
|
|
mant := ""
|
|
expStart := 0
|
|
if c == '0' && (peek(lx, 1) == 'x' || peek(lx, 1) == 'X') {
|
|
advance(lx)
|
|
advance(lx)
|
|
while lx.i < lx.src.len && (isHex(lx.src[lx.i]) || lx.src[lx.i] == '_') {
|
|
advance(lx)
|
|
}
|
|
} else {
|
|
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
|
|
advance(lx)
|
|
}
|
|
if peek(lx, 0) == '.' && isDigit(peek(lx, 1)) {
|
|
t.kind = "float"
|
|
advance(lx)
|
|
while lx.i < lx.src.len && (isDigit(lx.src[lx.i]) || lx.src[lx.i] == '_') {
|
|
advance(lx)
|
|
}
|
|
}
|
|
// an exponent: 1e9, 2.5e-3
|
|
e := peek(lx, 0)
|
|
sign := peek(lx, 1) == '+' || peek(lx, 1) == '-'
|
|
if (e == 'e' || e == 'E') && (isDigit(peek(lx, 1)) || (sign && isDigit(peek(lx, 2)))) {
|
|
if t.kind == "int" {
|
|
t.kind = "float"
|
|
mant = lx.src.sub(start, lx.i) + ".0"
|
|
}
|
|
expStart = lx.i
|
|
advance(lx)
|
|
if sign {
|
|
advance(lx)
|
|
}
|
|
while lx.i < lx.src.len && isDigit(lx.src[lx.i]) {
|
|
advance(lx)
|
|
}
|
|
}
|
|
}
|
|
t.text = lx.src.sub(start, lx.i).replace("_", "")
|
|
if mant.len > 0 {
|
|
t.text = (mant + lx.src.sub(expStart, lx.i)).replace("_", "") // LLVM wants the '.'
|
|
}
|
|
} else if c == '"' && peek(lx, 1) == '"' && peek(lx, 2) == '"' {
|
|
for tk in cook(lx, longString(lx, t), t) {
|
|
out.push(tk)
|
|
}
|
|
continue
|
|
} else if c == '"' {
|
|
for tk in cook(lx, rawString(lx, t), t) {
|
|
out.push(tk)
|
|
}
|
|
continue
|
|
} else if c == '"' || c == '\'' {
|
|
quote := c
|
|
t.kind = "string"
|
|
if quote == '\'' {
|
|
t.kind = "char"
|
|
}
|
|
advance(lx)
|
|
[]str parts
|
|
while peek(lx, 0) != quote {
|
|
if peek(lx, 0) == char(0) || peek(lx, 0) == '\n' {
|
|
if quote == '"' {
|
|
fail(lx, t.line, t.col, "string is never closed (missing \")")
|
|
}
|
|
fail(lx, t.line, t.col, "char is never closed (missing ')")
|
|
}
|
|
ch := peek(lx, 0)
|
|
advance(lx)
|
|
if ch == '\\' {
|
|
e := peek(lx, 0)
|
|
advance(lx)
|
|
ch = escape(lx, e)
|
|
}
|
|
parts.push(str(ch))
|
|
}
|
|
advance(lx)
|
|
t.text = parts.join("")
|
|
if quote == '\'' && t.text.len != 1 {
|
|
fail(lx, t.line, t.col, "a char holds exactly one character, like 'a'")
|
|
}
|
|
} else {
|
|
op := operator(lx)
|
|
if op.len == 0 {
|
|
// show the whole UTF-8 character, not just its first byte
|
|
n := 1
|
|
if (int(c) & 0xE0) == 0xC0 {
|
|
n = 2
|
|
} else if (int(c) & 0xF0) == 0xE0 {
|
|
n = 3
|
|
} else if (int(c) & 0xF8) == 0xF0 {
|
|
n = 4
|
|
}
|
|
hint := ""
|
|
if int(c) >= 0x80 {
|
|
hint = " (names use only a-z, A-Z, 0-9 and _; other letters only go inside \"text\")"
|
|
}
|
|
fail(lx, t.line, t.col, "unexpected character '" + lx.src.sub(lx.i, lx.i + n) + "'" + hint)
|
|
}
|
|
for k in 0..op.len {
|
|
advance(lx)
|
|
}
|
|
t.kind = op
|
|
t.text = op
|
|
}
|
|
out.push(t)
|
|
}
|
|
out.push(Token(kind: "eof", text: "", line: lx.line, col: lx.col, nl: true))
|
|
return out
|
|
}
|