Compare commits
No commits in common. "b42ceec686fed397760b219e7d0f29ff443027c0" and "3b6bdda0a42a78930bb3a396d5bc5daff5598db0" have entirely different histories.
b42ceec686
...
3b6bdda0a4
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,4 +28,3 @@ debug.log
|
|||||||
/compile_metadata_output.txt
|
/compile_metadata_output.txt
|
||||||
test_output*.txt
|
test_output*.txt
|
||||||
/site/src/version-template/lyng-version.js
|
/site/src/version-template/lyng-version.js
|
||||||
/bugs/
|
|
||||||
|
|||||||
@ -15,16 +15,15 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
|
|
||||||
## 2. Lexical Syntax
|
## 2. Lexical Syntax
|
||||||
- Comments: `// line`, `/* block */`.
|
- Comments: `// line`, `/* block */`.
|
||||||
- Strings: `"..."` or `` `...` `` (supports escapes). Multiline string content is normalized by indentation logic.
|
- Strings: `"..."` (supports escapes). Multiline string content is normalized by indentation logic.
|
||||||
- Shared escapes: `\n`, `\r`, `\t`, `\\`, `\uXXXX` (4 hex digits).
|
- Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits).
|
||||||
- Delimiter escapes: `\"` inside `"..."`, ``\` `` inside `` `...` ``.
|
|
||||||
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
|
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
|
||||||
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`).
|
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`).
|
||||||
- String interpolation is supported:
|
- String interpolation is supported:
|
||||||
- identifier form: `"$name"` or `` `$name` ``
|
- identifier form: `"$name"`
|
||||||
- expression form: `"${expr}"` or `` `${expr}` ``
|
- expression form: `"${expr}"`
|
||||||
- escaped dollar: `"\$"`, `"$$"`, `` `\$` ``, and `` `$$` `` all produce literal `$`.
|
- escaped dollar: `"\$"` and `"$$"` both produce literal `$`.
|
||||||
- `\\$x` means backslash + interpolated `x` in either delimiter form.
|
- `\\$x` means backslash + interpolated `x`.
|
||||||
- Per-file opt-out is supported via leading comment directive:
|
- Per-file opt-out is supported via leading comment directive:
|
||||||
- `// feature: interpolation: off`
|
- `// feature: interpolation: off`
|
||||||
- with this directive, `$...` stays literal text.
|
- with this directive, `$...` stays literal text.
|
||||||
|
|||||||
@ -1654,27 +1654,15 @@ The type for the character objects is `Char`.
|
|||||||
|
|
||||||
### String literal escapes
|
### String literal escapes
|
||||||
|
|
||||||
Lyng string literals can use either double quotes or backticks:
|
|
||||||
|
|
||||||
val a = "hello"
|
|
||||||
val b = `hello`
|
|
||||||
assert(a == b)
|
|
||||||
|
|
||||||
| escape | ASCII value |
|
| escape | ASCII value |
|
||||||
|--------|-----------------------|
|
|--------|-----------------------|
|
||||||
| \n | 0x10, newline |
|
| \n | 0x10, newline |
|
||||||
| \r | 0x13, carriage return |
|
| \r | 0x13, carriage return |
|
||||||
| \t | 0x07, tabulation |
|
| \t | 0x07, tabulation |
|
||||||
| \\ | \ slash character |
|
| \\ | \ slash character |
|
||||||
|
| \" | " double quote |
|
||||||
| \uXXXX | unicode code point |
|
| \uXXXX | unicode code point |
|
||||||
|
|
||||||
Delimiter-specific escapes:
|
|
||||||
|
|
||||||
| form | escape | value |
|
|
||||||
|--------|--------|------------------|
|
|
||||||
| `"..."` | \" | " double quote |
|
|
||||||
| `` `...` `` | \` | ` backtick |
|
|
||||||
|
|
||||||
Unicode escape form is exactly 4 hex digits, e.g. `"\u263A"` -> `☺`.
|
Unicode escape form is exactly 4 hex digits, e.g. `"\u263A"` -> `☺`.
|
||||||
|
|
||||||
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
Other `\c` combinations, where c is any char except mentioned above, are left intact, e.g.:
|
||||||
@ -1707,15 +1695,10 @@ Example:
|
|||||||
|
|
||||||
val name = "Lyng"
|
val name = "Lyng"
|
||||||
assertEquals("hello, Lyng!", "hello, $name!")
|
assertEquals("hello, Lyng!", "hello, $name!")
|
||||||
assertEquals("hello, Lyng!", `hello, $name!`)
|
|
||||||
assertEquals("sum=3", "sum=${1+2}")
|
assertEquals("sum=3", "sum=${1+2}")
|
||||||
assertEquals("sum=3", `sum=${1+2}`)
|
|
||||||
assertEquals("\$name", "\$name")
|
assertEquals("\$name", "\$name")
|
||||||
assertEquals("\$name", "$$name")
|
assertEquals("\$name", "$$name")
|
||||||
assertEquals("\$name", `\$name`)
|
|
||||||
assertEquals("\$name", `$$name`)
|
|
||||||
assertEquals("\\Lyng", "\\$name")
|
assertEquals("\\Lyng", "\\$name")
|
||||||
assertEquals("\\Lyng", `\\$name`)
|
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
Interpolation and `printf`-style formatting can be combined when needed:
|
Interpolation and `printf`-style formatting can be combined when needed:
|
||||||
|
|||||||
@ -310,7 +310,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
// Try literal and call-based receiver inference around the dot
|
// Try literal and call-based receiver inference around the dot
|
||||||
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
val i = TextCtx.prevNonWs(text, dotPos - 1)
|
||||||
val className: String? = when {
|
val className: String? = when {
|
||||||
i >= 0 && (text[i] == '"' || text[i] == '`') -> "String"
|
i >= 0 && text[i] == '"' -> "String"
|
||||||
i >= 0 && text[i] == ']' -> "List"
|
i >= 0 && text[i] == ']' -> "List"
|
||||||
i >= 0 && text[i] == '}' -> "Dict"
|
i >= 0 && text[i] == '}' -> "Dict"
|
||||||
i >= 0 && text[i] == ')' -> {
|
i >= 0 && text[i] == ')' -> {
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager
|
|||||||
import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor
|
import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor
|
||||||
import net.sergeych.lyng.format.LyngFormatConfig
|
import net.sergeych.lyng.format.LyngFormatConfig
|
||||||
import net.sergeych.lyng.format.LyngFormatter
|
import net.sergeych.lyng.format.LyngFormatter
|
||||||
import net.sergeych.lyng.format.LyngStringDelimiterPolicy
|
|
||||||
import net.sergeych.lyng.idea.LyngLanguage
|
import net.sergeych.lyng.idea.LyngLanguage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -171,7 +170,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
applySpacing = true,
|
applySpacing = true,
|
||||||
applyWrapping = false,
|
applyWrapping = false,
|
||||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
|
||||||
)
|
)
|
||||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text = doc.getText(r)
|
val text = doc.getText(r)
|
||||||
@ -191,7 +189,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
|
|||||||
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
|
||||||
applySpacing = settings.enableSpacing,
|
applySpacing = settings.enableSpacing,
|
||||||
applyWrapping = true,
|
applyWrapping = true,
|
||||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes,
|
|
||||||
)
|
)
|
||||||
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
|
||||||
val text = doc.getText(r)
|
val text = doc.getText(r)
|
||||||
|
|||||||
@ -101,8 +101,8 @@ class LyngLexer : LexerBase() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// String "...", `...`, or '...' with simple escape handling
|
// String "..." or '...' with simple escape handling
|
||||||
if (ch == '"' || ch == '\'' || ch == '`') {
|
if (ch == '"' || ch == '\'') {
|
||||||
val quote = ch
|
val quote = ch
|
||||||
i++
|
i++
|
||||||
while (i < endOffset) {
|
while (i < endOffset) {
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.lyng.idea.highlight
|
|
||||||
|
|
||||||
import org.junit.Assert.assertArrayEquals
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class LyngLexerBacktickStringTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun backtickStringGetsStringTokenAndColor() {
|
|
||||||
val lexer = LyngLexer()
|
|
||||||
val source = """val json = `{"name":"lyng","doc":"use \`quotes\`"}`"""
|
|
||||||
lexer.start(source, 0, source.length, 0)
|
|
||||||
|
|
||||||
val tokens = mutableListOf<Pair<String, String>>()
|
|
||||||
while (lexer.tokenType != null) {
|
|
||||||
val tokenText = source.substring(lexer.tokenStart, lexer.tokenEnd)
|
|
||||||
tokens += lexer.tokenType.toString() to tokenText
|
|
||||||
lexer.advance()
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
listOf(
|
|
||||||
"KEYWORD" to "val",
|
|
||||||
"WHITESPACE" to " ",
|
|
||||||
"IDENTIFIER" to "json",
|
|
||||||
"WHITESPACE" to " ",
|
|
||||||
"PUNCT" to "=",
|
|
||||||
"WHITESPACE" to " ",
|
|
||||||
"STRING" to "`{\"name\":\"lyng\",\"doc\":\"use \\`quotes\\`\"}`"
|
|
||||||
),
|
|
||||||
tokens
|
|
||||||
)
|
|
||||||
|
|
||||||
val highlighter = LyngSyntaxHighlighter()
|
|
||||||
assertArrayEquals(
|
|
||||||
arrayOf(LyngHighlighterColors.STRING),
|
|
||||||
highlighter.getTokenHighlights(LyngTokenTypes.STRING)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -420,7 +420,6 @@ private class Fmt : CoreCliktCommand(name = "fmt") {
|
|||||||
val cfg = net.sergeych.lyng.format.LyngFormatConfig(
|
val cfg = net.sergeych.lyng.format.LyngFormatConfig(
|
||||||
applySpacing = enableSpacing,
|
applySpacing = enableSpacing,
|
||||||
applyWrapping = enableWrapping,
|
applyWrapping = enableWrapping,
|
||||||
stringDelimiterPolicy = net.sergeych.lyng.format.LyngStringDelimiterPolicy.PreferFewerEscapes,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var anyChanged = false
|
var anyChanged = false
|
||||||
|
|||||||
@ -146,27 +146,18 @@ class CliLocalModuleImportRegressionJvmTest {
|
|||||||
val headers = Map<String, String>()
|
val headers = Map<String, String>()
|
||||||
|
|
||||||
fn startListen(port, host) {
|
fn startListen(port, host) {
|
||||||
var eager = Bravo()
|
|
||||||
eager.doSomething()
|
|
||||||
tcpServer = Net.tcpListen(port, host)
|
tcpServer = Net.tcpListen(port, host)
|
||||||
println("tcpServer.isOpen: " + tcpServer.isOpen())
|
// println("tcpServer.isOpen: " + tcpServer.isOpen()) // historical workaround; should not be needed
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
println("wait for accept...")
|
|
||||||
val tcpSocket = tcpServer.accept()
|
val tcpSocket = tcpServer.accept()
|
||||||
println("var bravo = Bravo()")
|
|
||||||
var bravo = Bravo()
|
var bravo = Bravo()
|
||||||
println("bravo.doSomething()...")
|
|
||||||
bravo.doSomething()
|
bravo.doSomething()
|
||||||
println("bravo.doSomething()... OK")
|
|
||||||
tcpSocket.close()
|
tcpSocket.close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
println("ERR [Alpha.startListen]: '", e, "'")
|
|
||||||
} finally {
|
} finally {
|
||||||
println("FIN [Alpha.startListen]")
|
|
||||||
tcpServer.close()
|
tcpServer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,7 +259,6 @@ class CliLocalModuleImportRegressionJvmTest {
|
|||||||
delay(50)
|
delay(50)
|
||||||
|
|
||||||
val socket = Net.tcpConnect("127.0.0.1", $port)
|
val socket = Net.tcpConnect("127.0.0.1", $port)
|
||||||
println("send ping...")
|
|
||||||
socket.writeUtf8("ping")
|
socket.writeUtf8("ping")
|
||||||
socket.flush()
|
socket.flush()
|
||||||
socket.close()
|
socket.close()
|
||||||
@ -279,10 +269,8 @@ class CliLocalModuleImportRegressionJvmTest {
|
|||||||
|
|
||||||
val result = runCli(mainFile.toString())
|
val result = runCli(mainFile.toString())
|
||||||
assertTrue(result.err.isBlank(), result.err)
|
assertTrue(result.err.isBlank(), result.err)
|
||||||
assertFalse(result.out.contains("ERR [Alpha.startListen]"), result.out)
|
|
||||||
assertFalse(result.out.contains("module capture 'Bravo'"), result.out)
|
assertFalse(result.out.contains("module capture 'Bravo'"), result.out)
|
||||||
assertTrue(result.out.contains("bravo.doSomething()... OK"), result.out)
|
assertTrue(result.out.contains("Bravo.doSomething"), result.out)
|
||||||
assertEquals(2, Regex("Bravo\\.doSomething").findAll(result.out).count(), result.out)
|
|
||||||
} finally {
|
} finally {
|
||||||
root.toFile().deleteRecursively()
|
root.toFile().deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,5 +17,4 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||||
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
|
||||||
|
|||||||
@ -2086,7 +2086,7 @@ class Compiler(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (scopeSeedNames.contains(name)) {
|
if (scopeSeedNames.contains(name)) {
|
||||||
val isModuleSlot = resolvesToModuleSeedSlot(name, slotLoc)
|
val isModuleSlot = modulePlan != null && slotLoc.scopeId == modulePlan.id
|
||||||
if (!isModuleSlot || useScopeSlots) return null
|
if (!isModuleSlot || useScopeSlots) return null
|
||||||
}
|
}
|
||||||
recordCaptureSlot(name, slotLoc)
|
recordCaptureSlot(name, slotLoc)
|
||||||
@ -2105,24 +2105,6 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolvesToModuleSeedSlot(name: String, slotLoc: SlotLocation): Boolean {
|
|
||||||
val modulePlan = moduleSlotPlan() ?: return false
|
|
||||||
var current: SlotLocation? = slotLoc
|
|
||||||
val visitedScopeIds = HashSet<Int>()
|
|
||||||
while (current != null && visitedScopeIds.add(current.scopeId)) {
|
|
||||||
if (current.scopeId == modulePlan.id) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val owner = capturePlanStack
|
|
||||||
.firstOrNull { it.slotPlan.id == current.scopeId }
|
|
||||||
?.captureOwners
|
|
||||||
?.get(name)
|
|
||||||
?: return false
|
|
||||||
current = owner
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun captureSlotRef(name: String, pos: Pos): ObjRef? {
|
private fun captureSlotRef(name: String, pos: Pos): ObjRef? {
|
||||||
if (capturePlanStack.isEmpty()) return null
|
if (capturePlanStack.isEmpty()) return null
|
||||||
if (name == "this") return null
|
if (name == "this") return null
|
||||||
|
|||||||
@ -361,7 +361,7 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
Token(":", from, Token.Type.COLON)
|
Token(":", from, Token.Type.COLON)
|
||||||
}
|
}
|
||||||
|
|
||||||
'"', '`' -> loadStringTokens(from, ch)
|
'"' -> loadStringTokens(from)
|
||||||
|
|
||||||
in digitsSet -> {
|
in digitsSet -> {
|
||||||
pos.back()
|
pos.back()
|
||||||
@ -550,11 +550,11 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
return fixed.joinToString("\n")
|
return fixed.joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadStringToken(delimiter: Char): Token {
|
private fun loadStringToken(): Token {
|
||||||
val start = currentPos
|
val start = currentPos
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
var newlineDetected = false
|
var newlineDetected = false
|
||||||
while (currentChar != delimiter) {
|
while (currentChar != '"') {
|
||||||
if (pos.end) throw ScriptError(start, "unterminated string started there")
|
if (pos.end) throw ScriptError(start, "unterminated string started there")
|
||||||
when (currentChar) {
|
when (currentChar) {
|
||||||
'\\' -> {
|
'\\' -> {
|
||||||
@ -572,8 +572,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
sb.append('\t'); pos.advance()
|
sb.append('\t'); pos.advance()
|
||||||
}
|
}
|
||||||
|
|
||||||
delimiter -> {
|
'"' -> {
|
||||||
sb.append(delimiter); pos.advance()
|
sb.append('"'); pos.advance()
|
||||||
}
|
}
|
||||||
|
|
||||||
'\\' -> {
|
'\\' -> {
|
||||||
@ -615,8 +615,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
data class Expr(val tokens: List<Token>, val pos: Pos) : StringChunk
|
data class Expr(val tokens: List<Token>, val pos: Pos) : StringChunk
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadStringTokens(startQuotePos: Pos, delimiter: Char): Token {
|
private fun loadStringTokens(startQuotePos: Pos): Token {
|
||||||
if (!interpolationEnabled) return loadStringToken(delimiter)
|
if (!interpolationEnabled) return loadStringToken()
|
||||||
val tokenPos = currentPos
|
val tokenPos = currentPos
|
||||||
|
|
||||||
val chunks = mutableListOf<StringChunk>()
|
val chunks = mutableListOf<StringChunk>()
|
||||||
@ -631,7 +631,7 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while (currentChar != delimiter) {
|
while (currentChar != '"') {
|
||||||
if (pos.end) throw ScriptError(startQuotePos, "unterminated string started there")
|
if (pos.end) throw ScriptError(startQuotePos, "unterminated string started there")
|
||||||
when (currentChar) {
|
when (currentChar) {
|
||||||
'\\' -> {
|
'\\' -> {
|
||||||
@ -649,8 +649,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
literal.append('\t'); pos.advance()
|
literal.append('\t'); pos.advance()
|
||||||
}
|
}
|
||||||
|
|
||||||
delimiter -> {
|
'"' -> {
|
||||||
literal.append(delimiter); pos.advance()
|
literal.append('"'); pos.advance()
|
||||||
}
|
}
|
||||||
|
|
||||||
'\\' -> {
|
'\\' -> {
|
||||||
@ -788,8 +788,8 @@ private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = t
|
|||||||
var depth = 1
|
var depth = 1
|
||||||
while (!pos.end) {
|
while (!pos.end) {
|
||||||
val ch = currentChar
|
val ch = currentChar
|
||||||
if (ch == '"' || ch == '`') {
|
if (ch == '"') {
|
||||||
appendQuoted(out, ch)
|
appendQuoted(out, '"')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (ch == '\'') {
|
if (ch == '\'') {
|
||||||
|
|||||||
@ -33,17 +33,6 @@ data class Pos(val source: Source, val line: Int, val column: Int) {
|
|||||||
if( end ) "EOF"
|
if( end ) "EOF"
|
||||||
else if( line >= 0 ) source.lines[line] else "<no line information>"
|
else if( line >= 0 ) source.lines[line] else "<no line information>"
|
||||||
|
|
||||||
val currentLineTrimmedStart: String get() = currentLine.trimStart()
|
|
||||||
|
|
||||||
val currentLineIndentWidth: Int
|
|
||||||
get() {
|
|
||||||
val lineText = currentLine
|
|
||||||
val firstNonWhitespace = lineText.indexOfFirst { !it.isWhitespace() }
|
|
||||||
return if (firstNonWhitespace >= 0) firstNonWhitespace else 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val visualColumn: Int get() = (column - currentLineIndentWidth).coerceAtLeast(0)
|
|
||||||
|
|
||||||
val end: Boolean get() = line >= source.lines.size
|
val end: Boolean get() = line >= source.lines.size
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -25,15 +25,14 @@ open class ScriptError(val pos: Pos, val errorMessage: String, cause: Throwable?
|
|||||||
"""
|
"""
|
||||||
$pos: Error: $errorMessage
|
$pos: Error: $errorMessage
|
||||||
|
|
||||||
${pos.currentLineTrimmedStart}
|
${pos.currentLine}
|
||||||
${if( pos.column >= 0 ) "-".repeat(pos.visualColumn) + "^" else ""}
|
${if( pos.column >= 0 ) "-".repeat(pos.column) + "^" else ""}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
cause
|
cause
|
||||||
)
|
)
|
||||||
|
|
||||||
class ScriptFlowIsNoMoreCollected: Exception()
|
class ScriptFlowIsNoMoreCollected: Exception()
|
||||||
|
|
||||||
class ExecutionError(val errorObject: Obj, pos: Pos, message: String, cause: Throwable? = null) :
|
class ExecutionError(val errorObject: Obj, pos: Pos, message: String) : ScriptError(pos, message)
|
||||||
ScriptError(pos, message, cause)
|
|
||||||
|
|
||||||
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
|
class ImportException(pos: Pos, message: String) : ScriptError(pos, message)
|
||||||
@ -3650,14 +3650,11 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileIndexRef(ref: IndexRef): CompiledValue? {
|
private fun compileIndexRef(ref: IndexRef): CompiledValue? {
|
||||||
val indexPos = refPosOrCurrent(ref.targetRef)
|
|
||||||
setPos(indexPos)
|
|
||||||
val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null
|
val receiver = compileRefWithFallback(ref.targetRef, null, Pos.builtIn) ?: return null
|
||||||
val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef)
|
val elementSlotType = indexElementSlotType(receiver.slot, ref.targetRef)
|
||||||
val dst = allocSlot()
|
val dst = allocSlot()
|
||||||
if (!ref.optionalRef) {
|
if (!ref.optionalRef) {
|
||||||
val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null
|
val index = compileRefWithFallback(ref.indexRef, null, Pos.builtIn) ?: return null
|
||||||
setPos(indexPos)
|
|
||||||
if (elementSlotType == SlotType.INT && index.type == SlotType.INT) {
|
if (elementSlotType == SlotType.INT && index.type == SlotType.INT) {
|
||||||
builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst)
|
builder.emit(Opcode.GET_INDEX_INT, receiver.slot, index.slot, dst)
|
||||||
updateSlotType(dst, SlotType.INT)
|
updateSlotType(dst, SlotType.INT)
|
||||||
|
|||||||
@ -43,10 +43,9 @@ class CmdVm {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
val throwable = frame.normalizeThrowable(e)
|
if (!frame.handleException(e)) {
|
||||||
if (!frame.handleException(throwable)) {
|
|
||||||
frame.cancelIterators()
|
frame.cancelIterators()
|
||||||
throw throwable
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4433,19 +4432,6 @@ class CmdFrame(
|
|||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun normalizeThrowable(t: Throwable): Throwable {
|
|
||||||
if (t is ExecutionError || t is ReturnException || t is LoopBreakContinueException) return t
|
|
||||||
val parentScope = ensureScope()
|
|
||||||
val pos = (t as? ScriptError)?.pos ?: currentErrorPos() ?: parentScope.pos
|
|
||||||
val throwScope = parentScope.createChildScope(pos = pos)
|
|
||||||
val message = when (t) {
|
|
||||||
is ScriptError -> t.errorMessage
|
|
||||||
else -> t.message ?: t.toString()
|
|
||||||
}
|
|
||||||
val errorObject = ObjUnknownException(throwScope, message).apply { getStackTrace() }
|
|
||||||
return ExecutionError(errorObject, pos, message, t)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun handleException(t: Throwable): Boolean {
|
suspend fun handleException(t: Throwable): Boolean {
|
||||||
val handler = tryStack.lastOrNull() ?: return false
|
val handler = tryStack.lastOrNull() ?: return false
|
||||||
vmIterDebug {
|
vmIterDebug {
|
||||||
|
|||||||
@ -16,11 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.sergeych.lyng.format
|
package net.sergeych.lyng.format
|
||||||
|
|
||||||
enum class LyngStringDelimiterPolicy {
|
|
||||||
Preserve,
|
|
||||||
PreferFewerEscapes,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatting configuration for Lyng source code.
|
* Formatting configuration for Lyng source code.
|
||||||
* Defaults are Kotlin-like.
|
* Defaults are Kotlin-like.
|
||||||
@ -33,7 +28,6 @@ data class LyngFormatConfig(
|
|||||||
val applySpacing: Boolean = false,
|
val applySpacing: Boolean = false,
|
||||||
val applyWrapping: Boolean = false,
|
val applyWrapping: Boolean = false,
|
||||||
val trailingComma: Boolean = false,
|
val trailingComma: Boolean = false,
|
||||||
val stringDelimiterPolicy: LyngStringDelimiterPolicy = LyngStringDelimiterPolicy.Preserve,
|
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
require(indentSize > 0) { "indentSize must be > 0" }
|
require(indentSize > 0) { "indentSize must be > 0" }
|
||||||
|
|||||||
@ -274,9 +274,7 @@ object LyngFormatter {
|
|||||||
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
fun format(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||||
// Phase 1: indentation
|
// Phase 1: indentation
|
||||||
val indented = reindent(text, config)
|
val indented = reindent(text, config)
|
||||||
if (!config.applySpacing && !config.applyWrapping &&
|
if (!config.applySpacing && !config.applyWrapping) return indented
|
||||||
config.stringDelimiterPolicy == LyngStringDelimiterPolicy.Preserve
|
|
||||||
) return indented
|
|
||||||
|
|
||||||
// Phase 2: minimal, safe spacing (PSI-free).
|
// Phase 2: minimal, safe spacing (PSI-free).
|
||||||
val lines = indented.split('\n')
|
val lines = indented.split('\n')
|
||||||
@ -288,27 +286,14 @@ object LyngFormatter {
|
|||||||
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
for (part in parts) {
|
for (part in parts) {
|
||||||
val normalizedPart = when (part.type) {
|
if (part.type == PartType.Code) {
|
||||||
PartType.Code -> if (config.applySpacing) applyMinimalSpacingRules(part.text) else part.text
|
sb.append(applyMinimalSpacingRules(part.text))
|
||||||
PartType.StringLiteral -> applyStringLiteralPolicy(part.text, config.stringDelimiterPolicy)
|
} else {
|
||||||
else -> part.text
|
sb.append(part.text)
|
||||||
}
|
}
|
||||||
sb.append(normalizedPart)
|
|
||||||
}
|
}
|
||||||
line = sb.toString()
|
line = sb.toString()
|
||||||
inBlockComment = nextInBlockComment
|
inBlockComment = nextInBlockComment
|
||||||
} else if (config.stringDelimiterPolicy != LyngStringDelimiterPolicy.Preserve) {
|
|
||||||
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
|
|
||||||
line = buildString(rawLine.length) {
|
|
||||||
for (part in parts) {
|
|
||||||
append(
|
|
||||||
if (part.type == PartType.StringLiteral) {
|
|
||||||
applyStringLiteralPolicy(part.text, config.stringDelimiterPolicy)
|
|
||||||
} else part.text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inBlockComment = nextInBlockComment
|
|
||||||
}
|
}
|
||||||
out.append(line.trimEnd())
|
out.append(line.trimEnd())
|
||||||
if (i < lines.lastIndex) out.append('\n')
|
if (i < lines.lastIndex) out.append('\n')
|
||||||
@ -478,84 +463,6 @@ object LyngFormatter {
|
|||||||
private enum class PartType { Code, StringLiteral, BlockComment, LineComment }
|
private enum class PartType { Code, StringLiteral, BlockComment, LineComment }
|
||||||
private data class Part(val text: String, val type: PartType)
|
private data class Part(val text: String, val type: PartType)
|
||||||
|
|
||||||
private fun applyStringLiteralPolicy(text: String, policy: LyngStringDelimiterPolicy): String {
|
|
||||||
if (policy == LyngStringDelimiterPolicy.Preserve) return text
|
|
||||||
if (text.length < 2) return text
|
|
||||||
val delimiter = text.first()
|
|
||||||
if (delimiter != '"' && delimiter != '`') return text
|
|
||||||
if (text.last() != delimiter) return text
|
|
||||||
val other = if (delimiter == '"') '`' else '"'
|
|
||||||
val rewritten = rewriteStringLiteralDelimiter(text, other) ?: return text
|
|
||||||
return when (policy) {
|
|
||||||
LyngStringDelimiterPolicy.Preserve -> text
|
|
||||||
LyngStringDelimiterPolicy.PreferFewerEscapes -> {
|
|
||||||
val currentCost = delimiterEscapeCost(text, delimiter)
|
|
||||||
val rewrittenCost = delimiterEscapeCost(rewritten, other)
|
|
||||||
if (rewrittenCost < currentCost) rewritten else text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun delimiterEscapeCost(text: String, delimiter: Char): Int {
|
|
||||||
var cost = 0
|
|
||||||
var i = 1
|
|
||||||
while (i < text.length - 1) {
|
|
||||||
val ch = text[i]
|
|
||||||
if (ch == '\\' && i + 1 < text.length - 1) {
|
|
||||||
val next = text[i + 1]
|
|
||||||
if (next == delimiter) cost++
|
|
||||||
i += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch == delimiter) cost++
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return cost
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun rewriteStringLiteralDelimiter(text: String, targetDelimiter: Char): String? {
|
|
||||||
if (text.length < 2) return null
|
|
||||||
val sourceDelimiter = text.first()
|
|
||||||
if ((sourceDelimiter != '"' && sourceDelimiter != '`') || text.last() != sourceDelimiter) return null
|
|
||||||
if (sourceDelimiter == targetDelimiter) return text
|
|
||||||
val body = StringBuilder(text.length + 8)
|
|
||||||
var i = 1
|
|
||||||
val end = text.length - 1
|
|
||||||
while (i < end) {
|
|
||||||
val ch = text[i]
|
|
||||||
if (ch == '\\' && i + 1 < end) {
|
|
||||||
val next = text[i + 1]
|
|
||||||
when {
|
|
||||||
next == sourceDelimiter -> {
|
|
||||||
if (sourceDelimiter == targetDelimiter) body.append('\\').append(targetDelimiter)
|
|
||||||
else body.append(next)
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
next == targetDelimiter -> {
|
|
||||||
body.append('\\').append('\\').append('\\').append(targetDelimiter)
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
body.append(ch).append(next)
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (ch == targetDelimiter) {
|
|
||||||
body.append('\\').append(targetDelimiter)
|
|
||||||
} else {
|
|
||||||
body.append(ch)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return buildString(body.length + 2) {
|
|
||||||
append(targetDelimiter)
|
|
||||||
append(body)
|
|
||||||
append(targetDelimiter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a line into parts: code, string literals, and comments.
|
* Split a line into parts: code, string literals, and comments.
|
||||||
* Tracks [inBlockComment] state across lines.
|
* Tracks [inBlockComment] state across lines.
|
||||||
@ -607,7 +514,7 @@ private fun splitIntoParts(
|
|||||||
inBlockComment = true
|
inBlockComment = true
|
||||||
last = i
|
last = i
|
||||||
i += 2
|
i += 2
|
||||||
} else if (text[i] == '"' || text[i] == '\'' || text[i] == '`') {
|
} else if (text[i] == '"' || text[i] == '\'') {
|
||||||
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
|
if (i > last) result.add(Part(text.substring(last, i), PartType.Code))
|
||||||
inString = true
|
inString = true
|
||||||
quoteChar = text[i]
|
quoteChar = text[i]
|
||||||
|
|||||||
@ -143,10 +143,7 @@ class SimpleLyngHighlighter : LyngHighlighter {
|
|||||||
val k = kindOf(t.type, t.value) ?: continue
|
val k = kindOf(t.type, t.value) ?: continue
|
||||||
val start0 = src.offsetOf(t.pos)
|
val start0 = src.offsetOf(t.pos)
|
||||||
val range = when (t.type) {
|
val range = when (t.type) {
|
||||||
Type.STRING, Type.STRING2 -> {
|
Type.STRING, Type.STRING2 -> adjustQuoteSpan(start0, '"')
|
||||||
val quote = text.getOrNull(start0)?.takeIf { it == '"' || it == '`' } ?: '"'
|
|
||||||
adjustQuoteSpan(start0, quote)
|
|
||||||
}
|
|
||||||
Type.CHAR -> adjustQuoteSpan(start0, '\'')
|
Type.CHAR -> adjustQuoteSpan(start0, '\'')
|
||||||
Type.HEX -> {
|
Type.HEX -> {
|
||||||
// Parser returns HEX token value without the leading "0x"; include it in highlight span
|
// Parser returns HEX token value without the leading "0x"; include it in highlight span
|
||||||
|
|||||||
@ -530,7 +530,7 @@ object DocLookupUtils {
|
|||||||
var inString = false
|
var inString = false
|
||||||
while (i < text.length) {
|
while (i < text.length) {
|
||||||
val ch = text[i]
|
val ch = text[i]
|
||||||
if ((ch == '"' || ch == '`') && (i == 0 || text[i - 1] != '\\')) {
|
if (ch == '"' && (i == 0 || text[i - 1] != '\\')) {
|
||||||
inString = !inString
|
inString = !inString
|
||||||
}
|
}
|
||||||
if (!inString && ch == '/' && i + 1 < text.length) {
|
if (!inString && ch == '/' && i + 1 < text.length) {
|
||||||
|
|||||||
@ -106,18 +106,16 @@ open class ObjException(
|
|||||||
val pos = s.pos
|
val pos = s.pos
|
||||||
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
if (pos != lastPos && !pos.currentLine.isEmpty()) {
|
||||||
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
|
if (lastPos == null || (lastPos.source != pos.source || lastPos.line != pos.line)) {
|
||||||
val sourceLine = pos.currentLineTrimmedStart
|
|
||||||
val visualColumn = pos.visualColumn
|
|
||||||
val fallback =
|
val fallback =
|
||||||
ObjString("#${pos.source.objSourceName}:${pos.line+1}:${visualColumn+1}: $sourceLine")
|
ObjString("#${pos.source.objSourceName}:${pos.line+1}:${pos.column+1}: ${pos.currentLine}")
|
||||||
if (maybeCls != null) {
|
if (maybeCls != null) {
|
||||||
try {
|
try {
|
||||||
result.list += maybeCls.callWithArgs(
|
result.list += maybeCls.callWithArgs(
|
||||||
scope,
|
scope,
|
||||||
pos.source.objSourceName,
|
pos.source.objSourceName,
|
||||||
ObjInt(pos.line.toLong()),
|
ObjInt(pos.line.toLong()),
|
||||||
ObjInt(visualColumn.toLong()),
|
ObjInt(pos.column.toLong()),
|
||||||
ObjString(sourceLine)
|
ObjString(pos.currentLine)
|
||||||
)
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Fallback textual entry if StackTraceEntry fails to instantiate
|
// Fallback textual entry if StackTraceEntry fails to instantiate
|
||||||
|
|||||||
@ -4395,30 +4395,6 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun backtickStringsMatchRegularStringSemantics() = runTest {
|
|
||||||
val d = '$'
|
|
||||||
eval(
|
|
||||||
"""
|
|
||||||
val name = "Lyng"
|
|
||||||
val simple = `hello, $d{name}`
|
|
||||||
assertEquals("hello, Lyng", simple)
|
|
||||||
assertEquals("{\"name\":\"Lyng\"}", `{"name":"Lyng"}`)
|
|
||||||
assertEquals("use the `code` style", `use the \`code\` style`)
|
|
||||||
assertEquals("\\\"", `\"`)
|
|
||||||
assertEquals("\"", `"`)
|
|
||||||
assertEquals(
|
|
||||||
"first\n\"second\"\nthird",
|
|
||||||
`
|
|
||||||
first
|
|
||||||
"second"
|
|
||||||
third
|
|
||||||
`
|
|
||||||
)
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInlineArrayLiteral() = runTest {
|
fun testInlineArrayLiteral() = runTest {
|
||||||
eval(
|
eval(
|
||||||
@ -5261,35 +5237,6 @@ class ScriptTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testUnexpectedThrowablePreservesThrowSiteStackTrace() = runTest {
|
|
||||||
val caught = evalNamed(
|
|
||||||
"baderrorstack", """
|
|
||||||
fun boom() {
|
|
||||||
val arr = [10, 20, 30]
|
|
||||||
arr[10]
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
boom()
|
|
||||||
"unreachable"
|
|
||||||
} catch (e) {
|
|
||||||
e
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
val trace = caught.getLyngExceptionMessageWithStackTrace()
|
|
||||||
assertContains(trace, "\n at baderrorstack:3:1: arr[10]")
|
|
||||||
assertContains(trace, "\n at baderrorstack:6:1: boom()")
|
|
||||||
assertFalse(trace.contains("catch (e)"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testScriptErrorMessageTrimsSourceIndent() = runTest {
|
|
||||||
val x = ScriptError(Pos(Source("trimraw", " arr[10]"), 0, 4), "boom")
|
|
||||||
assertContains(x.message!!, "\narr[10]\n")
|
|
||||||
assertFalse(x.message!!.contains("\n arr[10]\n"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMapIteralAmbiguity() = runTest {
|
fun testMapIteralAmbiguity() = runTest {
|
||||||
eval(
|
eval(
|
||||||
|
|||||||
@ -31,13 +31,6 @@ class UnicodeEscapeTest {
|
|||||||
assertEquals("☺", token.value)
|
assertEquals("☺", token.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parserDecodesUnicodeEscapeInBacktickStringLiteral() {
|
|
||||||
val token = parseLyng("`\\u263A`".toSource()).first()
|
|
||||||
assertEquals(Token.Type.STRING, token.type)
|
|
||||||
assertEquals("☺", token.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parserDecodesUnicodeEscapeInCharLiteral() {
|
fun parserDecodesUnicodeEscapeInCharLiteral() {
|
||||||
val token = parseLyng("'\\u263A'".toSource()).first()
|
val token = parseLyng("'\\u263A'".toSource()).first()
|
||||||
@ -62,7 +55,6 @@ class UnicodeEscapeTest {
|
|||||||
@Test
|
@Test
|
||||||
fun evalDecodesUnicodeEscapes() = runTest {
|
fun evalDecodesUnicodeEscapes() = runTest {
|
||||||
assertEquals(ObjString("☺"), eval("\"\\u263A\""))
|
assertEquals(ObjString("☺"), eval("\"\\u263A\""))
|
||||||
assertEquals(ObjString("☺"), eval("`\\u263A`"))
|
|
||||||
assertEquals(ObjChar('☺'), eval("'\\u263A'"))
|
assertEquals(ObjChar('☺'), eval("'\\u263A'"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,8 +22,6 @@ import net.sergeych.lyng.bridge.globalBinder
|
|||||||
import net.sergeych.lyng.obj.ObjVoid
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class PrintlnOverrideTest {
|
class PrintlnOverrideTest {
|
||||||
|
|
||||||
@ -86,46 +84,4 @@ class PrintlnOverrideTest {
|
|||||||
|
|
||||||
assertEquals(listOf("gb top level", "gb inside function"), output)
|
assertEquals(listOf("gb top level", "gb inside function"), output)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testExceptionPrintStackTraceFormatsPrimaryFrameBlock() = runTest {
|
|
||||||
val scope = Script.newScope()
|
|
||||||
val output = mutableListOf<String>()
|
|
||||||
|
|
||||||
scope.globalBinder().bindGlobalFun("println") {
|
|
||||||
val sb = StringBuilder()
|
|
||||||
for (i in 0 until args.size) {
|
|
||||||
if (i > 0) sb.append(" ")
|
|
||||||
sb.append(string(i))
|
|
||||||
}
|
|
||||||
output.add(sb.toString())
|
|
||||||
ObjVoid
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.eval(
|
|
||||||
"""
|
|
||||||
fun boom() {
|
|
||||||
val arr = [10, 20, 30]
|
|
||||||
var a = 10
|
|
||||||
var b = arr[a]
|
|
||||||
b
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
boom()
|
|
||||||
} catch (e) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
assertTrue(output.isNotEmpty())
|
|
||||||
assertEquals(
|
|
||||||
"IndexOutOfBoundsException: Index 10 out of bounds for length 3 at eval:4:9:",
|
|
||||||
output[0]
|
|
||||||
)
|
|
||||||
assertEquals("var b = arr[a]", output[1])
|
|
||||||
assertEquals("--------^", output[2])
|
|
||||||
assertTrue(output.size >= 4)
|
|
||||||
assertFalse(output.any { it == " at eval:4:9: var b = arr[a]" })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,36 +18,9 @@ package net.sergeych.lyng.format
|
|||||||
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
class LyngFormatterTest {
|
class LyngFormatterTest {
|
||||||
|
|
||||||
@Test
|
|
||||||
fun preferBackticksForQuoteHeavyStrings() {
|
|
||||||
val src = "val json = \"{\\\"name\\\":\\\"lyng\\\",\\\"kind\\\":\\\"lang\\\"}\""
|
|
||||||
val cfg = LyngFormatConfig(
|
|
||||||
applySpacing = true,
|
|
||||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes
|
|
||||||
)
|
|
||||||
|
|
||||||
val out = LyngFormatter.format(src, cfg)
|
|
||||||
|
|
||||||
assertEquals("""val json = `{"name":"lyng","kind":"lang"}`""", out)
|
|
||||||
assertEquals(out, LyngFormatter.format(out, cfg))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun preserveStringsWhenAlternativeWouldNotHelp() {
|
|
||||||
val src = "val sample = \"use `ticks` and keep \\` literal\""
|
|
||||||
val cfg = LyngFormatConfig(
|
|
||||||
stringDelimiterPolicy = LyngStringDelimiterPolicy.PreferFewerEscapes
|
|
||||||
)
|
|
||||||
|
|
||||||
val out = LyngFormatter.format(src, cfg)
|
|
||||||
|
|
||||||
assertEquals(src, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun labelFormatting() {
|
fun labelFormatting() {
|
||||||
val src = "return @label; break @outer; continue @inner"
|
val src = "return @label; break @outer; continue @inner"
|
||||||
@ -106,9 +79,9 @@ class LyngFormatterTest {
|
|||||||
|
|
||||||
val formatted = LyngFormatter.format(src, LyngFormatConfig(applyWrapping = true, maxLineLength = 40, continuationIndentSize = 4))
|
val formatted = LyngFormatter.format(src, LyngFormatConfig(applyWrapping = true, maxLineLength = 40, continuationIndentSize = 4))
|
||||||
// Ensure the string literal remains intact
|
// Ensure the string literal remains intact
|
||||||
assertTrue(formatted.contains(arg2), "String literal must be preserved")
|
kotlin.test.assertTrue(formatted.contains(arg2), "String literal must be preserved")
|
||||||
// Ensure end-of-line comment remains
|
// Ensure end-of-line comment remains
|
||||||
assertTrue(formatted.contains("// end comment"), "EOL comment must be preserved")
|
kotlin.test.assertTrue(formatted.contains("// end comment"), "EOL comment must be preserved")
|
||||||
// Idempotency
|
// Idempotency
|
||||||
val formatted2 = LyngFormatter.format(formatted, LyngFormatConfig(applyWrapping = true, maxLineLength = 40, continuationIndentSize = 4))
|
val formatted2 = LyngFormatter.format(formatted, LyngFormatConfig(applyWrapping = true, maxLineLength = 40, continuationIndentSize = 4))
|
||||||
assertEquals(formatted, formatted2)
|
assertEquals(formatted, formatted2)
|
||||||
|
|||||||
@ -17,5 +17,4 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||||
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
|
||||||
|
|||||||
@ -17,5 +17,4 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? =
|
internal actual fun objListBoundsViolationMessageOrNull(size: Int, index: Int): String? = null
|
||||||
if (index < 0 || index >= size) "Index $index out of bounds for length $size" else null
|
|
||||||
|
|||||||
@ -441,25 +441,9 @@ static fun List<T>.fill(size: Int, block: (Int)->T): List<T> {
|
|||||||
|
|
||||||
/* Print this exception and its stack trace to standard output. */
|
/* Print this exception and its stack trace to standard output. */
|
||||||
fun Exception.printStackTrace(): void {
|
fun Exception.printStackTrace(): void {
|
||||||
if( stackTrace.size == 0 ) {
|
println(this)
|
||||||
println(this)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val first = stackTrace[0] as StackTraceEntry
|
|
||||||
var arrow = "^"
|
|
||||||
for( i in 0..<first.column ) {
|
|
||||||
arrow = "-" + arrow
|
|
||||||
}
|
|
||||||
println("%s: %s at %s:"(this::class.className, message, first.at))
|
|
||||||
println(first.sourceString)
|
|
||||||
println(arrow)
|
|
||||||
var skipFirst = true
|
|
||||||
for( entry in stackTrace ) {
|
for( entry in stackTrace ) {
|
||||||
if( skipFirst ) {
|
println("\tat "+entry.toString())
|
||||||
skipFirst = false
|
|
||||||
} else {
|
|
||||||
println(" at " + entry.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user