From 5fc0969491c567a9f622da427908aef6501bbb3c Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 6 Jan 2026 01:51:16 +0100 Subject: [PATCH] added object expression support to the site, tnbundle, etc --- docs/whats_new.md | 16 ++++++++++++++++ .../lyng-textmate/syntaxes/lyng.tmLanguage.json | 6 +++--- .../sergeych/lyng/idea/highlight/LyngLexer.kt | 6 +++--- .../kotlin/net/sergeych/lyngweb/Highlight.kt | 10 +++++++--- site/src/jsMain/kotlin/HomePage.kt | 8 ++++++-- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/docs/whats_new.md b/docs/whats_new.md index 4c59ca7..e362659 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -105,6 +105,22 @@ object Config { Config.show() ``` +### Object Expressions +You can now create anonymous objects that inherit from classes or interfaces using the `object : Base { ... }` syntax. These expressions capture their lexical scope and support multiple inheritance. + +```lyng +val worker = object : Runnable { + override fun run() = println("Working...") +} + +val x = object : Base(arg1), Interface1 { + val property = 42 + override fun method() = this@object.property * 2 +} +``` + +Use `this@object` to refer to the innermost anonymous object instance when `this` is rebound. + ### Unified Delegation Model A powerful new delegation system allows `val`, `var`, and `fun` members to delegate their logic to other objects using the `by` keyword. diff --git a/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json b/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json index 8d80d79..644446d 100644 --- a/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json +++ b/editors/lyng-textmate/syntaxes/lyng.tmLanguage.json @@ -76,9 +76,9 @@ }, "labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] }, "directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] }, - "declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum|interface)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] }, - "keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|interface|val|var|import|package|constructor|property|abstract|override|open|closed|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] }, - "constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] }, + "declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum|interface|object)(?:\\s+([\\p{L}_][\\p{L}\\p{N}_]*))?", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] }, + "keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|interface|val|var|import|package|constructor|property|abstract|override|open|closed|extern|private|protected|static|get|set|object|init|by)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] }, + "constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this(?:@[\\p{L}_][\\p{L}\\p{N}_]*)?)\\b|π)" } ] }, "types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] }, "operators": { "patterns": [ { "name": "keyword.operator.comparison.lyng", "match": "===|!==|==|!=|<=|>=|<|>" }, { "name": "keyword.operator.shuttle.lyng", "match": "<=>" }, { "name": "keyword.operator.arrow.lyng", "match": "=>|->|::" }, { "name": "keyword.operator.range.lyng", "match": "\\.\\.\\.|\\.\\.<|\\.\\." }, { "name": "keyword.operator.nullsafe.lyng", "match": "\\?\\.|\\?\\[|\\?\\(|\\?\\{|\\?:|\\?\\?" }, { "name": "keyword.operator.assignment.lyng", "match": "(?:\\+=|-=|\\*=|/=|%=|=)" }, { "name": "keyword.operator.logical.lyng", "match": "&&|\\|\\|" }, { "name": "keyword.operator.bitwise.lyng", "match": "<<|>>|&|\\||\\^|~" }, { "name": "keyword.operator.match.lyng", "match": "=~|!~" }, { "name": "keyword.operator.arithmetic.lyng", "match": "\\+\\+|--|[+\\-*/%]" }, { "name": "keyword.operator.other.lyng", "match": "[!?]" } ] }, "punctuation": { "patterns": [ { "name": "punctuation.separator.comma.lyng", "match": "," }, { "name": "punctuation.terminator.statement.lyng", "match": ";" }, { "name": "punctuation.section.block.begin.lyng", "match": "[(]{1}|[{]{1}|\\[" }, { "name": "punctuation.section.block.end.lyng", "match": "[)]{1}|[}]{1}|\\]" }, { "name": "punctuation.accessor.dot.lyng", "match": "\\." }, { "name": "punctuation.separator.colon.lyng", "match": ":" } ] } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt index 22c7c1d..5e04ed8 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/highlight/LyngLexer.kt @@ -33,10 +33,10 @@ class LyngLexer : LexerBase() { private val keywords = setOf( "fun", "val", "var", "class", "interface", "type", "import", "as", - "abstract", "closed", "override", + "abstract", "closed", "override", "static", "extern", "open", "private", "protected", "if", "else", "for", "while", "return", "true", "false", "null", "when", "in", "is", "break", "continue", "try", "catch", "finally", - "get", "set" + "get", "set", "object", "enum", "init", "by", "property", "constructor" ) override fun start(buffer: CharSequence, startOffset: Int, endOffset: Int, initialState: Int) { @@ -160,5 +160,5 @@ class LyngLexer : LexerBase() { private fun Char.isDigit(): Boolean = this in '0'..'9' private fun Char.isIdentifierStart(): Boolean = this == '_' || this.isLetter() private fun Char.isIdentifierPart(): Boolean = this.isIdentifierStart() || this.isDigit() - private fun isPunct(c: Char): Boolean = c in setOf('(', ')', '{', '}', '[', ']', '.', ',', ';', ':', '+', '-', '*', '/', '%', '=', '<', '>', '!', '?', '&', '|', '^', '~') + private fun isPunct(c: Char): Boolean = c in setOf('(', ')', '{', '}', '[', ']', '.', ',', ';', ':', '+', '-', '*', '/', '%', '=', '<', '>', '!', '?', '&', '|', '^', '~', '@') } diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt index adb85e8..bfff5aa 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/Highlight.kt @@ -530,7 +530,7 @@ private fun detectDeclarationAndParamOverrides(text: String): Map fun isIdentPart(ch: Char) = ch == '_' || ch == '$' || ch == '~' || ch.isLetterOrDigit() // A conservative list of language keywords to avoid misclassifying as function calls val kw = setOf( - "package", "import", "fun", "fn", "class", "interface", "enum", "val", "var", + "package", "import", "fun", "fn", "class", "interface", "enum", "object", "val", "var", "if", "else", "while", "do", "for", "when", "try", "catch", "finally", "throw", "return", "break", "continue", "in", "is", "as", "as?", "not", "true", "false", "null", "private", "protected", "abstract", "closed", "override", "open", "extern", "static", @@ -640,8 +640,12 @@ private fun detectDeclarationAndParamOverrides(text: String): Map i = p continue } - if (text.startsWith("class", i) && (i == 0 || !isIdentPart(text[i-1])) && (i+5 >= n || !isIdentPart(text.getOrNull(i+5) ?: '\u0000'))) { - var p = skipWs(i + 5) + if ((text.startsWith("class", i) && (i == 0 || !isIdentPart(text[i-1])) && (i+5 >= n || !isIdentPart(text.getOrNull(i+5) ?: '\u0000'))) || + (text.startsWith("object", i) && (i == 0 || !isIdentPart(text[i-1])) && (i+6 >= n || !isIdentPart(text.getOrNull(i+6) ?: '\u0000'))) || + (text.startsWith("enum", i) && (i == 0 || !isIdentPart(text[i-1])) && (i+4 >= n || !isIdentPart(text.getOrNull(i+4) ?: '\u0000'))) || + (text.startsWith("interface", i) && (i == 0 || !isIdentPart(text[i-1])) && (i+9 >= n || !isIdentPart(text.getOrNull(i+9) ?: '\u0000')))) { + val kwLen = if (text.startsWith("class", i)) 5 else if (text.startsWith("object", i)) 6 else if (text.startsWith("enum", i)) 4 else 9 + var p = skipWs(i + kwLen) val name = readIdent(p) if (name != null) result[p to name.second] = "hl-class" i = p diff --git a/site/src/jsMain/kotlin/HomePage.kt b/site/src/jsMain/kotlin/HomePage.kt index cea20bd..7f31cbd 100644 --- a/site/src/jsMain/kotlin/HomePage.kt +++ b/site/src/jsMain/kotlin/HomePage.kt @@ -86,8 +86,12 @@ val base = { a: 1, b: 2 } val patch = { b: 3, c: } val m = { "a": 0, ...base, ...patch, d: 4 } assertEquals(1, m["a"]) // base overwrites 0 -assertEquals(3, m["b"]) // patch overwrites base -assertEquals(4, m["d"]) // literal key + +// Object expressions: anonymous classes on the fly +val worker = object : Runnable { + override fun run() = println("Working...") +} +worker.run() >>> void """.trimIndent() val mapHtml = "
" + htmlEscape(code) + "
"