diff --git a/Sources/Compiler/Parse/Lexer.swift b/Sources/Compiler/Parse/Lexer.swift index abf83b2..37a48bc 100644 --- a/Sources/Compiler/Parse/Lexer.swift +++ b/Sources/Compiler/Parse/Lexer.swift @@ -178,13 +178,20 @@ struct Lexer { currentColumn += 1 } } + + /// Gets `currentIndex` through a noninlined call. Used to work around + /// an optimizer bug using a stale value of `currentIndex`. + @inline(never) + private func currentIndexOptimizerWorkaround() -> String.Index { + return currentIndex + } // SQLite does not seem to really care what goes between the escape delimiters. // Table names will gladly take newlines and such. private mutating func parseEscapedIdentifier(closing: Character) -> Token { let tokenStart = startLocation() advance() // Opening - let identifierStart = currentIndex + let identifierStart = currentIndexOptimizerWorkaround() while let current, current != closing { advance() @@ -323,7 +330,7 @@ struct Lexer { advance() // 0 advance() // x or X - let numberStart = currentIndex + let numberStart = currentIndexOptimizerWorkaround() while let current, Lexer.hexDigits.contains(current) || current == "_" { advance() @@ -345,8 +352,8 @@ struct Lexer { private mutating func parseStringContents() -> (Substring, SourceLocation) { let tokenStart = startLocation() advance() - let stringStart = currentIndex - + let stringStart = currentIndexOptimizerWorkaround() + while let current, current != "'" { advance() } diff --git a/Tests/CompilerTests/LexerSliceReleaseTests.swift b/Tests/CompilerTests/LexerSliceReleaseTests.swift new file mode 100644 index 0000000..51ae5f8 --- /dev/null +++ b/Tests/CompilerTests/LexerSliceReleaseTests.swift @@ -0,0 +1,81 @@ +// +// LexerSliceReleaseTests.swift +// +// +// Created by Wes Wickwire on 6/27/26. +// + +import Testing +@testable import Compiler + +/// These tests only failed in a release build. +@Suite +struct LexerSliceReleaseTests { + @Test func escapedIdentifierStripsDelimiters() { + #expect(firstIdentifier("\"MyModel.ID\"") == "MyModel.ID") + #expect(firstIdentifier("[MyModel.ID]") == "MyModel.ID") + #expect(firstIdentifier("`MyModel.ID`") == "MyModel.ID") + } + + @Test func stringLiteralStripsQuotes() { + #expect(firstString("'hello world'") == "hello world") + } + + @Test func hexLiteral() { + #expect(firstHex("0xFF") == 255) + } + + @Test func scientificNotation() { + #expect(firstDouble("1e3") == 1000) + #expect(firstDouble("1e-2") == 0.01) + } + + @Test func aliasedColumnTypeGeneratesUnquotedName() { + var compiler = Compiler() + let (_, diags) = compiler.compile(migration: """ + CREATE TABLE myTable ( + id INTEGER AS "MyModel.ID" + ); + """) + #expect(diags.isEmpty) + let table = compiler.schema.tables.values.first! + let col = table.columns.values.first! + #expect(!String(describing: col.type).contains("\"")) + } + + private func firstIdentifier(_ src: String) -> String? { + var lexer = Lexer(source: src) + while true { + let t = lexer.next() + if case .eof = t.kind { return nil } + if case let .identifier(v) = t.kind { return String(v) } + } + } + + private func firstString(_ src: String) -> String? { + var lexer = Lexer(source: src) + while true { + let t = lexer.next() + if case .eof = t.kind { return nil } + if case let .string(v) = t.kind { return String(v) } + } + } + + private func firstHex(_ src: String) -> Int? { + var lexer = Lexer(source: src) + while true { + let t = lexer.next() + if case .eof = t.kind { return nil } + if case let .hex(v) = t.kind { return v } + } + } + + private func firstDouble(_ src: String) -> Double? { + var lexer = Lexer(source: src) + while true { + let t = lexer.next() + if case .eof = t.kind { return nil } + if case let .double(v) = t.kind { return v } + } + } +}