added clamp function and extensions, fixed bug in range coloring
This commit is contained in:
parent
0759346e4b
commit
5dc2159024
@ -98,10 +98,11 @@ Exclusive end char ranges are supported too:
|
|||||||
| isEndInclusive | true for '..' | Bool |
|
| isEndInclusive | true for '..' | Bool |
|
||||||
| isOpen | at any end | Bool |
|
| isOpen | at any end | Bool |
|
||||||
| isIntRange | both start and end are Int | Bool |
|
| isIntRange | both start and end are Int | Bool |
|
||||||
| start | | Bool |
|
| start | | Any? |
|
||||||
| end | | Bool |
|
| end | | Any? |
|
||||||
| size | for finite ranges, see above | Long |
|
| size | for finite ranges, see above | Long |
|
||||||
| [] | see above | |
|
| [] | see above | |
|
||||||
| | | |
|
|
||||||
|
Ranges are also used with the `clamp(value, range)` function and the `value.clamp(range)` extension method to limit values within boundaries.
|
||||||
|
|
||||||
[Iterable]: Iterable.md
|
[Iterable]: Iterable.md
|
||||||
@ -19,6 +19,7 @@ you can use it's class to ensure type:
|
|||||||
|-----------------|-------------------------------------------------------------|------|
|
|-----------------|-------------------------------------------------------------|------|
|
||||||
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
||||||
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
|
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
|
||||||
|
| `.clamp(range)` | clamp value within range boundaries | Real |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
| | | |
|
| | | |
|
||||||
|
|||||||
@ -92,6 +92,7 @@ or transformed `Real` otherwise.
|
|||||||
| pow(x, y) | ${x^y}$ |
|
| pow(x, y) | ${x^y}$ |
|
||||||
| sqrt(x) | $ \sqrt {x}$ |
|
| sqrt(x) | $ \sqrt {x}$ |
|
||||||
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
|
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
|
||||||
|
| clamp(x, range) | limit x to be inside range boundaries |
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@ -102,6 +103,11 @@ For example:
|
|||||||
// abs() keeps the argument type:
|
// abs() keeps the argument type:
|
||||||
assert( abs(-1) is Int)
|
assert( abs(-1) is Int)
|
||||||
assert( abs(-2.21) == 2.21 )
|
assert( abs(-2.21) == 2.21 )
|
||||||
|
|
||||||
|
// clamp() limits value to the range:
|
||||||
|
assert( clamp(15, 0..10) == 10 )
|
||||||
|
assert( clamp(-5, 0..10) == 0 )
|
||||||
|
assert( 5.clamp(0..10) == 5 )
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
## Scientific constant
|
## Scientific constant
|
||||||
|
|||||||
@ -186,6 +186,26 @@ Key features:
|
|||||||
- **Structural Equality**: Transient fields are automatically ignored during `==` equality checks.
|
- **Structural Equality**: Transient fields are automatically ignored during `==` equality checks.
|
||||||
- **Deserialization**: Transient constructor parameters with default values are correctly restored to those defaults upon restoration.
|
- **Deserialization**: Transient constructor parameters with default values are correctly restored to those defaults upon restoration.
|
||||||
|
|
||||||
|
### Value Clamping (`clamp`)
|
||||||
|
A new `clamp()` function has been added to the standard library to limit a value within a specified range. It is available as both a global function and an extension method on all objects.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
// Global function
|
||||||
|
clamp(15, 0..10) // returns 10
|
||||||
|
clamp(-5, 0..10) // returns 0
|
||||||
|
|
||||||
|
// Extension method
|
||||||
|
val x = 15
|
||||||
|
x.clamp(0..10) // returns 10
|
||||||
|
|
||||||
|
// Exclusive and open-ended ranges
|
||||||
|
15.clamp(0..<10) // returns 9
|
||||||
|
15.clamp(..10) // returns 10
|
||||||
|
-5.clamp(0..) // returns 0
|
||||||
|
```
|
||||||
|
|
||||||
|
`clamp()` correctly handles inclusive (`..`) and exclusive (`..<`) ranges. For discrete types like `Int` and `Char`, clamping to an exclusive upper bound returns the previous value.
|
||||||
|
|
||||||
## Tooling and Infrastructure
|
## Tooling and Infrastructure
|
||||||
|
|
||||||
### CLI: Formatting Command
|
### CLI: Formatting Command
|
||||||
|
|||||||
@ -139,7 +139,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val e: Int = miniSource.offsetOf(d.range.end)
|
val e: Int = miniSource.offsetOf(d.range.end)
|
||||||
if (offset >= s && offset <= e) {
|
if (offset >= s && offset <= e) {
|
||||||
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
||||||
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>"
|
return renderTitle("enum constant ${d.name}.${name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -361,7 +361,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
"Print values to the standard output and append a newline. Accepts any number of arguments." else
|
"Print values to the standard output and append a newline. Accepts any number of arguments." else
|
||||||
"Print values to the standard output without a trailing newline. Accepts any number of arguments."
|
"Print values to the standard output without a trailing newline. Accepts any number of arguments."
|
||||||
val title = "function $ident(values)"
|
val title = "function $ident(values)"
|
||||||
return "<div class='doc-title'>${htmlEscape(title)}</div>" + styledMarkdown(htmlEscape(fallback))
|
return renderTitle(title) + styledMarkdown(htmlEscape(fallback))
|
||||||
}
|
}
|
||||||
// 4b) try class members like ClassName.member with inheritance fallback
|
// 4b) try class members like ClassName.member with inheritance fallback
|
||||||
val lhs = previousWordBefore(text, idRange.startOffset)
|
val lhs = previousWordBefore(text, idRange.startOffset)
|
||||||
@ -519,7 +519,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(d.doc))
|
sb.append(renderDocBody(d.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@ -527,7 +527,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
|
|
||||||
// Find matching @param tag
|
// Find matching @param tag
|
||||||
fn.doc?.tags?.get("param")?.forEach { tag ->
|
fn.doc?.tags?.get("param")?.forEach { tag ->
|
||||||
@ -549,7 +549,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
|
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(m.doc))
|
sb.append(renderDocBody(m.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@ -560,7 +560,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(m.doc))
|
sb.append(renderDocBody(m.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@ -579,6 +579,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return "(${params})${ret}"
|
return "(${params})${ret}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renderTitle(title: String): String {
|
||||||
|
return "<div class='doc-title' style='margin-bottom: 0.8em;'>${htmlEscape(title)}</div>"
|
||||||
|
}
|
||||||
|
|
||||||
private fun htmlEscape(s: String): String = buildString(s.length) {
|
private fun htmlEscape(s: String): String = buildString(s.length) {
|
||||||
for (ch in s) append(
|
for (ch in s) append(
|
||||||
when (ch) {
|
when (ch) {
|
||||||
@ -645,7 +649,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
|
|
||||||
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
|
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>Overloads for ").append(htmlEscape(name)).append("</div>")
|
sb.append(renderTitle("Overloads for $name"))
|
||||||
sb.append("<ul>")
|
sb.append("<ul>")
|
||||||
overloads.forEach { fn ->
|
overloads.forEach { fn ->
|
||||||
sb.append("<li><code>")
|
sb.append("<li><code>")
|
||||||
|
|||||||
@ -121,12 +121,39 @@ class LyngLexer : LexerBase() {
|
|||||||
|
|
||||||
// Number
|
// Number
|
||||||
if (ch.isDigit()) {
|
if (ch.isDigit()) {
|
||||||
|
// Check for hex: 0x...
|
||||||
|
if (ch == '0' && i + 1 < endOffset && buffer[i + 1] == 'x') {
|
||||||
|
i += 2
|
||||||
|
while (i < endOffset && (buffer[i].isDigit() || buffer[i] in 'a'..'f' || buffer[i] in 'A'..'F')) i++
|
||||||
|
myTokenEnd = i
|
||||||
|
myTokenType = LyngTokenTypes.NUMBER
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimal or integer
|
||||||
i++
|
i++
|
||||||
var hasDot = false
|
var hasDot = false
|
||||||
|
var hasE = false
|
||||||
while (i < endOffset) {
|
while (i < endOffset) {
|
||||||
val c = buffer[i]
|
val c = buffer[i]
|
||||||
if (c.isDigit()) { i++; continue }
|
if (c.isDigit() || c == '_') {
|
||||||
if (c == '.' && !hasDot) { hasDot = true; i++; continue }
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (c == '.' && !hasDot && !hasE) {
|
||||||
|
// Check if it's a fractional part (must be followed by a digit)
|
||||||
|
if (i + 1 < endOffset && buffer[i + 1].isDigit()) {
|
||||||
|
hasDot = true
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((c == 'e' || c == 'E') && !hasE) {
|
||||||
|
hasE = true
|
||||||
|
i++
|
||||||
|
if (i < endOffset && (buffer[i] == '+' || buffer[i] == '-')) i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
myTokenEnd = i
|
myTokenEnd = i
|
||||||
@ -161,6 +188,35 @@ class LyngLexer : LexerBase() {
|
|||||||
// Punctuation
|
// Punctuation
|
||||||
if (isPunct(ch)) {
|
if (isPunct(ch)) {
|
||||||
i++
|
i++
|
||||||
|
// Handle common multi-char operators for better highlighting
|
||||||
|
when (ch) {
|
||||||
|
'.' -> {
|
||||||
|
if (i < endOffset && buffer[i] == '.') {
|
||||||
|
i++
|
||||||
|
if (i < endOffset && (buffer[i] == '.' || buffer[i] == '<')) i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'=' -> {
|
||||||
|
if (i < endOffset && (buffer[i] == '=' || buffer[i] == '>' || buffer[i] == '~')) {
|
||||||
|
i++
|
||||||
|
if (buffer[i - 1] == '=' && i < endOffset && buffer[i] == '=') i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'+', '-', '*', '/', '%', '!', '<', '>', '&', '|', '?', ':', '^' -> {
|
||||||
|
if (i < endOffset) {
|
||||||
|
val next = buffer[i]
|
||||||
|
if (next == '=' || next == ch) {
|
||||||
|
i++
|
||||||
|
if (ch == '<' && next == '=' && i < endOffset && buffer[i] == '>') i++
|
||||||
|
if (ch == '!' && next == '=' && i < endOffset && buffer[i] == '=') i++
|
||||||
|
} else if (ch == '?' && (next == '.' || next == '[' || next == '(' || next == '{' || next == ':' || next == '?')) {
|
||||||
|
i++
|
||||||
|
} else if (ch == '-' && next == '>') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
myTokenEnd = i
|
myTokenEnd = i
|
||||||
myTokenType = LyngTokenTypes.PUNCT
|
myTokenType = LyngTokenTypes.PUNCT
|
||||||
return
|
return
|
||||||
|
|||||||
@ -20,10 +20,7 @@ package net.sergeych.lyng
|
|||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||||
import net.sergeych.lyng.miniast.addConstDoc
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
|
||||||
import net.sergeych.lyng.miniast.addVoidFnDoc
|
|
||||||
import net.sergeych.lyng.miniast.type
|
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
@ -162,6 +159,42 @@ class Script(
|
|||||||
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
|
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addFnDoc(
|
||||||
|
"clamp",
|
||||||
|
doc = "Clamps the value within the specified range. If the value is outside the range, it is set to the nearest boundary. Respects inclusive/exclusive range ends.",
|
||||||
|
params = listOf(ParamDoc("value"), ParamDoc("range")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
|
val value = requiredArg<Obj>(0)
|
||||||
|
val range = requiredArg<ObjRange>(1)
|
||||||
|
|
||||||
|
var result = value
|
||||||
|
if (range.start != null && !range.start.isNull) {
|
||||||
|
if (result.compareTo(this, range.start) < 0) {
|
||||||
|
result = range.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (range.end != null && !range.end.isNull) {
|
||||||
|
val cmp = range.end.compareTo(this, result)
|
||||||
|
if (range.isEndInclusive) {
|
||||||
|
if (cmp < 0) result = range.end
|
||||||
|
} else {
|
||||||
|
if (cmp <= 0) {
|
||||||
|
if (range.end is ObjInt) {
|
||||||
|
result = ObjInt.of(range.end.value - 1)
|
||||||
|
} else if (range.end is ObjChar) {
|
||||||
|
result = ObjChar((range.end.value.code - 1).toChar())
|
||||||
|
} else {
|
||||||
|
// For types where we can't easily find "previous" value (like Real),
|
||||||
|
// we just return the exclusive boundary as a fallback.
|
||||||
|
result = range.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
addVoidFn("assert") {
|
addVoidFn("assert") {
|
||||||
val cond = requiredArg<ObjBool>(0)
|
val cond = requiredArg<ObjBool>(0)
|
||||||
val message = if (args.size > 1)
|
val message = if (args.size > 1)
|
||||||
|
|||||||
@ -760,6 +760,38 @@ open class Obj {
|
|||||||
) {
|
) {
|
||||||
thisObj.toJson(this).toString().toObj()
|
thisObj.toJson(this).toString().toObj()
|
||||||
}
|
}
|
||||||
|
addFnDoc(
|
||||||
|
name = "clamp",
|
||||||
|
doc = "Clamps this value within the specified range. If the value is outside the range, it is set to the nearest boundary. Respects inclusive/exclusive range ends.",
|
||||||
|
params = listOf(ParamDoc("range")),
|
||||||
|
moduleName = "lyng.stdlib"
|
||||||
|
) {
|
||||||
|
val range = requiredArg<ObjRange>(0)
|
||||||
|
|
||||||
|
var result = thisObj
|
||||||
|
if (range.start != null && !range.start.isNull) {
|
||||||
|
if (result.compareTo(this, range.start) < 0) {
|
||||||
|
result = range.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (range.end != null && !range.end.isNull) {
|
||||||
|
val cmp = range.end.compareTo(this, result)
|
||||||
|
if (range.isEndInclusive) {
|
||||||
|
if (cmp < 0) result = range.end
|
||||||
|
} else {
|
||||||
|
if (cmp <= 0) {
|
||||||
|
if (range.end is ObjInt) {
|
||||||
|
result = ObjInt.of(range.end.value - 1)
|
||||||
|
} else if (range.end is ObjChar) {
|
||||||
|
result = ObjChar((range.end.value.code - 1).toChar())
|
||||||
|
} else {
|
||||||
|
result = range.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4986,5 +4986,41 @@ class ScriptTest {
|
|||||||
assertEquals(["bar"], A().f2("bar"))
|
assertEquals(["bar"], A().f2("bar"))
|
||||||
""")
|
""")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClamp() = runTest {
|
||||||
|
eval("""
|
||||||
|
// Global clamp
|
||||||
|
assertEquals(5, clamp(5, 0..10))
|
||||||
|
assertEquals(0, clamp(-5, 0..10))
|
||||||
|
assertEquals(10, clamp(15, 0..10))
|
||||||
|
|
||||||
|
// Extension clamp
|
||||||
|
assertEquals(5, 5.clamp(0..10))
|
||||||
|
assertEquals(0, (-5).clamp(0..10))
|
||||||
|
assertEquals(10, 15.clamp(0..10))
|
||||||
|
|
||||||
|
// Exclusive range
|
||||||
|
assertEquals(9, 15.clamp(0..<10))
|
||||||
|
assertEquals(0, (-5).clamp(0..<10))
|
||||||
|
|
||||||
|
// Open-ended range
|
||||||
|
assertEquals(10, 15.clamp(..10))
|
||||||
|
assertEquals(-5, (-5).clamp(..10))
|
||||||
|
assertEquals(5, 5.clamp(0..))
|
||||||
|
assertEquals(0, (-5).clamp(0..))
|
||||||
|
|
||||||
|
// Character range
|
||||||
|
assertEquals('e', 'e'.clamp('a'..'z'))
|
||||||
|
assertEquals('a', ' '.clamp('a'..'z'))
|
||||||
|
assertEquals('z', '}'.clamp('a'..'z'))
|
||||||
|
assertEquals('y', '}'.clamp('a'..<'z'))
|
||||||
|
|
||||||
|
// Real numbers (boundaries are inclusive in current impl for Real)
|
||||||
|
assertEquals(5.5, 5.5.clamp(0.0..10.0))
|
||||||
|
assertEquals(0.0, (-1.5).clamp(0.0..10.0))
|
||||||
|
assertEquals(10.0, 15.5.clamp(0.0..10.0))
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user