feat: string interpolation via f"..." syntax
Bootstrap compiler:
- Add ekStringInterp AST node with text/expr parts
- Lexer: recognize f"..." prefix for interpolation strings
- Parser: split f"..." into ekStringInterp, parse inner expressions
via sub-lexer/sub-parser. Supports escaped braces \{ and \}.
- Sema: type-check interpolated expressions, return String type
- HIR lowerer: desugar ekStringInterp to chained String_Concat
calls with automatic type conversions:
- int/uint types -> String_FromInt
- float types -> String_FromFloat
- bool -> String_FromBool
- String -> as-is
Selfhost compiler:
- ast.bux: add ekStringInterp constant (reserved for future)
- lexer.bux: accept f" prefix, treat as plain string literal
(selfhost parser does not implement interpolation yet)
Tests:
- _test_string_interp verifies int, float, bool, String,
multiple interpolations, plain strings, and Fmt-style {0} templates
This commit is contained in:
@@ -127,6 +127,7 @@ type
|
||||
ekBorrow ## borrow &mut expr — explicit borrow expression
|
||||
ekBlock
|
||||
ekMatch
|
||||
ekStringInterp
|
||||
|
||||
MatchArm* = object
|
||||
loc*: SourceLocation
|
||||
@@ -219,6 +220,9 @@ type
|
||||
of ekMatch:
|
||||
exprMatchSubject*: Expr
|
||||
exprMatchArms*: seq[MatchArm]
|
||||
of ekStringInterp:
|
||||
exprInterpTexts*: seq[string]
|
||||
exprInterpExprs*: seq[Expr]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Statements
|
||||
@@ -458,3 +462,6 @@ proc newBinaryExpr*(op: TokenKind, left, right: Expr, loc: SourceLocation): Expr
|
||||
|
||||
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
|
||||
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
|
||||
|
||||
proc newStringInterpExpr*(texts: seq[string], exprs: seq[Expr], loc: SourceLocation): Expr =
|
||||
result = Expr(kind: ekStringInterp, loc: loc, exprInterpTexts: texts, exprInterpExprs: exprs)
|
||||
|
||||
@@ -1092,6 +1092,47 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
# The borrow checker validates before lowering
|
||||
return ctx.lowerExpr(expr.exprBorrowOperand)
|
||||
|
||||
of ekStringInterp:
|
||||
# Desugar string interpolation to chained String_Concat calls with conversions
|
||||
var resultNode: HirNode = nil
|
||||
for i in 0 ..< expr.exprInterpExprs.len:
|
||||
let textPart = expr.exprInterpTexts[i]
|
||||
let exprPart = expr.exprInterpExprs[i]
|
||||
# Text literal
|
||||
var textNode = HirNode(kind: hLit,
|
||||
litToken: Token(kind: tkStringLiteral, text: "\"" & textPart & "\"", loc: loc),
|
||||
typ: makeStr(), loc: loc)
|
||||
if resultNode == nil:
|
||||
resultNode = textNode
|
||||
else:
|
||||
resultNode = hirCall("String_Concat", @[resultNode, textNode], makeStr(), loc)
|
||||
# Expression part with conversion if needed
|
||||
let loweredExpr = ctx.lowerExpr(exprPart)
|
||||
let exprType = ctx.resolveExprType(exprPart)
|
||||
var convertedExpr = loweredExpr
|
||||
if exprType.kind == tkInt or exprType.kind == tkInt8 or exprType.kind == tkInt16 or
|
||||
exprType.kind == tkInt32 or exprType.kind == tkInt64 or
|
||||
exprType.kind == tkUInt or exprType.kind == tkUInt8 or exprType.kind == tkUInt16 or
|
||||
exprType.kind == tkUInt32 or exprType.kind == tkUInt64:
|
||||
convertedExpr = hirCall("String_FromInt", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkFloat32 or exprType.kind == tkFloat64:
|
||||
convertedExpr = hirCall("String_FromFloat", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkBool:
|
||||
convertedExpr = hirCall("String_FromBool", @[loweredExpr], makeStr(), loc)
|
||||
elif exprType.kind == tkStr:
|
||||
discard # already a string
|
||||
resultNode = hirCall("String_Concat", @[resultNode, convertedExpr], makeStr(), loc)
|
||||
# Add final text part
|
||||
let lastText = expr.exprInterpTexts[^1]
|
||||
var lastTextNode = HirNode(kind: hLit,
|
||||
litToken: Token(kind: tkStringLiteral, text: "\"" & lastText & "\"", loc: loc),
|
||||
typ: makeStr(), loc: loc)
|
||||
if resultNode == nil:
|
||||
resultNode = lastTextNode
|
||||
else:
|
||||
resultNode = hirCall("String_Concat", @[resultNode, lastTextNode], makeStr(), loc)
|
||||
return resultNode
|
||||
|
||||
else:
|
||||
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc),
|
||||
typ: makeVoid(), loc: loc)
|
||||
|
||||
+4
-1
@@ -527,7 +527,10 @@ proc nextToken(lex: var Lexer): Token =
|
||||
discard lex.advance()
|
||||
return Token(kind: tkNewLine, text: "\n", loc: startLoc)
|
||||
|
||||
# String prefixes: c8" c16" c32" — must come before ident check
|
||||
# String prefixes: f" c8" c16" c32" — must come before ident check
|
||||
if c == 'f' and lex.peek(1) == '"':
|
||||
discard lex.advance() # f
|
||||
return lex.scanString(startLoc, 1)
|
||||
if c == 'c' and lex.peek(1) in {'8', '1', '3'}:
|
||||
let d = lex.peek(1)
|
||||
if d == '8' and lex.peek(2) == '"':
|
||||
|
||||
+72
-2
@@ -1,5 +1,5 @@
|
||||
import std/strutils
|
||||
import token, source_location, ast
|
||||
import token, source_location, ast, lexer
|
||||
|
||||
type
|
||||
ParserDiagnosticSeverity* = enum
|
||||
@@ -48,6 +48,12 @@ proc advance(p: var Parser): Token =
|
||||
if p.pos < p.tokens.len:
|
||||
inc p.pos
|
||||
|
||||
proc hasErrors*(p: Parser): bool =
|
||||
for d in p.diagnostics:
|
||||
if d.severity == pdsError:
|
||||
return true
|
||||
return false
|
||||
|
||||
proc check(p: Parser, kind: TokenKind): bool =
|
||||
p.peek() == kind
|
||||
|
||||
@@ -391,13 +397,77 @@ proc parseAssign(p: var Parser): Expr
|
||||
proc parseExpr(p: var Parser): Expr =
|
||||
p.parseAssign()
|
||||
|
||||
proc parseStringInterpolation(p: var Parser, tok: Token): Expr =
|
||||
## Parse a string literal that contains {expr} interpolations.
|
||||
let text = tok.text
|
||||
# Only f"..." strings support interpolation; plain "...", backtick, and c8/c16/c32 are raw.
|
||||
if text.len < 3 or text[0] != 'f' or text[1] != '"' or text[^1] != '"':
|
||||
return newLiteralExpr(tok)
|
||||
let inner = text[2 ..< ^1] # strip f" ... "
|
||||
var texts: seq[string] = @[]
|
||||
var exprs: seq[Expr] = @[]
|
||||
var i = 0
|
||||
var currentText = ""
|
||||
while i < inner.len:
|
||||
if inner[i] == '\\' and i + 1 < inner.len:
|
||||
if inner[i + 1] == '{':
|
||||
currentText.add('{')
|
||||
i += 2
|
||||
continue
|
||||
elif inner[i + 1] == '}':
|
||||
currentText.add('}')
|
||||
i += 2
|
||||
continue
|
||||
elif inner[i] == '{':
|
||||
texts.add(currentText)
|
||||
currentText = ""
|
||||
var j = i + 1
|
||||
var depth = 1
|
||||
while j < inner.len and depth > 0:
|
||||
if inner[j] == '{': depth += 1
|
||||
elif inner[j] == '}': depth -= 1
|
||||
j += 1
|
||||
if depth != 0:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "unmatched '{' in string interpolation"))
|
||||
return newLiteralExpr(tok)
|
||||
let exprStr = inner[i + 1 ..< j - 1]
|
||||
if exprStr.len == 0:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "empty interpolation {}"))
|
||||
return newLiteralExpr(tok)
|
||||
let lexRes = tokenize(exprStr, p.sourceName & "<interp>")
|
||||
if lexRes.hasErrors:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "invalid expression in string interpolation: " & exprStr))
|
||||
return newLiteralExpr(tok)
|
||||
var subParser = initParser(lexRes.tokens, p.sourceName & "<interp>")
|
||||
let expr = subParser.parseExpr()
|
||||
if subParser.hasErrors:
|
||||
for d in subParser.diagnostics:
|
||||
p.diagnostics.add(ParserDiagnostic(severity: pdsError, loc: tok.loc,
|
||||
message: "interpolation parse error: " & d.message))
|
||||
return newLiteralExpr(tok)
|
||||
exprs.add(expr)
|
||||
i = j
|
||||
continue
|
||||
currentText.add(inner[i])
|
||||
i += 1
|
||||
texts.add(currentText)
|
||||
if exprs.len == 0:
|
||||
return newLiteralExpr(tok)
|
||||
return newStringInterpExpr(texts, exprs, tok.loc)
|
||||
|
||||
proc parsePrimary(p: var Parser): Expr =
|
||||
while p.check(tkNewLine):
|
||||
discard p.advance()
|
||||
let loc = p.currentLoc
|
||||
case p.peek()
|
||||
of tkIntLiteral, tkFloatLiteral, tkStringLiteral, tkCharLiteral, tkBoolLiteral:
|
||||
return newLiteralExpr(p.advance())
|
||||
let tok = p.advance()
|
||||
if tok.kind == tkStringLiteral:
|
||||
return parseStringInterpolation(p, tok)
|
||||
return newLiteralExpr(tok)
|
||||
of tkSelf:
|
||||
discard p.advance()
|
||||
return Expr(kind: ekSelf, loc: loc)
|
||||
|
||||
@@ -1264,6 +1264,10 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
return operand
|
||||
of ekSpread:
|
||||
return sema.checkExpr(expr.exprSpreadOperand, scope)
|
||||
of ekStringInterp:
|
||||
for e in expr.exprInterpExprs:
|
||||
discard sema.checkExpr(e, scope)
|
||||
return makeStr()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Statement type checking
|
||||
|
||||
Reference in New Issue
Block a user