diff --git a/bootstrap/ast.nim b/bootstrap/ast.nim index e352716..09811e0 100644 --- a/bootstrap/ast.nim +++ b/bootstrap/ast.nim @@ -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) diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index ffb29eb..ed4da6c 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -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) diff --git a/bootstrap/lexer.nim b/bootstrap/lexer.nim index 9fc41f3..762a3ad 100644 --- a/bootstrap/lexer.nim +++ b/bootstrap/lexer.nim @@ -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) == '"': diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index d280fe4..b15e70f 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -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 & "") + 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 & "") + 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) diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 603a7da..367f7ae 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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 diff --git a/src/ast.bux b/src/ast.bux index 2d9c357..cf309a4 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -95,6 +95,7 @@ const ekBlock: int = 21; const ekMatch: int = 22; const ekSpawn: int = 24; const ekAwait: int = 25; +const ekStringInterp: int = 26; struct ExprList { expr: *Expr, diff --git a/src/lexer.bux b/src/lexer.bux index 35f5f2f..74951d8 100644 --- a/src/lexer.bux +++ b/src/lexer.bux @@ -704,7 +704,12 @@ func lexNextToken(lex: *Lexer) { return; } - // String prefixes: c8" c16" c32" + // String prefixes: f" c8" c16" c32" + if c == 102 && lexPeek(lex, 1) == 34 { // f" + discard lexAdvance(lex); // f + lexMarkStart(lex); // treat as plain string literal in selfhost + lexScanString(lex); return; + } if c == 99 { // 'c' let d: uint32 = lexPeek(lex, 1); if d == 56 && lexPeek(lex, 2) == 34 { // c8"