Compare commits
4 Commits
e107296bca
...
53a9d21a19
| Author | SHA1 | Date | |
|---|---|---|---|
| 53a9d21a19 | |||
| 739fdfc94b | |||
| c8e03d69ad | |||
| b2200e71ff |
16
docs/OOP.md
16
docs/OOP.md
@ -479,6 +479,18 @@ val block: context(Html, Head) Body.()->String = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Context receivers can also constrain extension functions. The extension is visible only when the required receiver is
|
||||||
|
already in the implicit receiver stack:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Tag { fun addText(text: String) { /* ... */ } }
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
this@Tag.addText(this)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- Field inheritance (`val`/`var`) and collisions
|
- Field inheritance (`val`/`var`) and collisions
|
||||||
- Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base).
|
- Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base).
|
||||||
- Qualified read/write (via `this@Type` or casts) targets the chosen ancestor’s storage.
|
- Qualified read/write (via `this@Type` or casts) targets the chosen ancestor’s storage.
|
||||||
@ -650,10 +662,14 @@ Unary operators are overloaded by defining methods with no arguments:
|
|||||||
|
|
||||||
| Operator | Method Name |
|
| Operator | Method Name |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
|
| `+a` | `unaryPlus()` |
|
||||||
| `-a` | `negate()` |
|
| `-a` | `negate()` |
|
||||||
| `!a` | `logicalNot()` |
|
| `!a` | `logicalNot()` |
|
||||||
| `~a` | `bitNot()` |
|
| `~a` | `bitNot()` |
|
||||||
|
|
||||||
|
`unaryPlus()` is useful in DSL-style builders where `+"text"` should append text to
|
||||||
|
the current receiver. See [samples/html_builder_dsl.lyng](samples/html_builder_dsl.lyng).
|
||||||
|
|
||||||
### Assignment Operators
|
### Assignment Operators
|
||||||
|
|
||||||
Assignment operators like `+=` first attempt to call a specific assignment method. If that method is not defined, they fall back to a combination of the binary operator and a regular assignment (e.g., `a = a + b`).
|
Assignment operators like `+=` first attempt to call a specific assignment method. If that method is not defined, they fall back to a combination of the binary operator and a regular assignment (e.g., `a = a + b`).
|
||||||
|
|||||||
@ -83,6 +83,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
## 4. Operators (implemented)
|
## 4. Operators (implemented)
|
||||||
- Assignment: `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `?=`.
|
- Assignment: `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `?=`.
|
||||||
- Logical: `||`, `&&`, unary `!`.
|
- Logical: `||`, `&&`, unary `!`.
|
||||||
|
- Unary arithmetic/bitwise: unary `+`, unary `-`, `~`.
|
||||||
- Bitwise: `|`, `^`, `&`, `~`, shifts `<<`, `>>`.
|
- Bitwise: `|`, `^`, `&`, `~`, shifts `<<`, `>>`.
|
||||||
- Equality/comparison: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`, `<=>`, `=~`, `!~`.
|
- Equality/comparison: `==`, `!=`, `===`, `!==`, `<`, `<=`, `>`, `>=`, `<=>`, `=~`, `!~`.
|
||||||
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
- Type/containment: `is`, `!is`, `in`, `!in`, `as`, `as?`.
|
||||||
@ -119,6 +120,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- shorthand: `fun f(x) = expr`.
|
- shorthand: `fun f(x) = expr`.
|
||||||
- generics: `fun f<T>(x: T): T`.
|
- generics: `fun f<T>(x: T): T`.
|
||||||
- extension functions: `fun Type.name(...) { ... }`.
|
- extension functions: `fun Type.name(...) { ... }`.
|
||||||
|
- context-aware extension functions: `context(Tag) fun String.unaryPlus() { this@Tag.addText(this) }`.
|
||||||
- named singleton `object` declarations can be extension receivers too: `fun Config.describe(...) { ... }`, `val Config.tag get() = ...`.
|
- named singleton `object` declarations can be extension receivers too: `fun Config.describe(...) { ... }`, `val Config.tag get() = ...`.
|
||||||
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
|
- static extension functions are callable on the type object: `static fun List<T>.fill(...)` -> `List.fill(...)`.
|
||||||
- delegated callable: `fun f(...) by delegate`.
|
- delegated callable: `fun f(...) by delegate`.
|
||||||
|
|||||||
@ -93,6 +93,7 @@ Requires installing `lyngio` into the import manager from host code.
|
|||||||
- `import lyng.io.http.server` (minimal HTTP/1.1 and WebSocket server API)
|
- `import lyng.io.http.server` (minimal HTTP/1.1 and WebSocket server API)
|
||||||
- `import lyng.io.ws` (WebSocket client API; currently supported on JVM, capability-gated elsewhere)
|
- `import lyng.io.ws` (WebSocket client API; currently supported on JVM, capability-gated elsewhere)
|
||||||
- `import lyng.io.net` (TCP/UDP transport API; currently supported on JVM, capability-gated elsewhere)
|
- `import lyng.io.net` (TCP/UDP transport API; currently supported on JVM, capability-gated elsewhere)
|
||||||
|
- `import lyng.io.html` (pure Lyng HTML builder DSL: `html { body { h3 { +"text" } } }`)
|
||||||
- Shared network value-type packages are also available when installed by host code:
|
- Shared network value-type packages are also available when installed by host code:
|
||||||
- `import lyng.io.http.types` (`HttpHeaders`)
|
- `import lyng.io.http.types` (`HttpHeaders`)
|
||||||
- `import lyng.io.ws.types` (`WsMessage`)
|
- `import lyng.io.ws.types` (`WsMessage`)
|
||||||
|
|||||||
@ -27,6 +27,6 @@ See `docs/lyng_d_files.md` for `.lyng.d` syntax and examples.
|
|||||||
- Alternatively, if/when the plugin is published to a marketplace, you will be able to install it
|
- Alternatively, if/when the plugin is published to a marketplace, you will be able to install it
|
||||||
directly from the “Marketplace” tab (not yet available).
|
directly from the “Marketplace” tab (not yet available).
|
||||||
|
|
||||||
### [Download plugin v0.0.2-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.2-SNAPSHOT.zip)
|
### [Download plugin v0.0.5-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.5-SNAPSHOT.zip)
|
||||||
|
|
||||||
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
||||||
|
|||||||
164
docs/lyng.io.html.md
Normal file
164
docs/lyng.io.html.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
# lyng.io.html
|
||||||
|
|
||||||
|
`lyng.io.html` provides a pure Lyng HTML builder DSL. It uses Lyng context
|
||||||
|
receiver extensions, so text can be appended with `+"text"` inside tag blocks
|
||||||
|
without global builder state.
|
||||||
|
|
||||||
|
Host code installs the package from `lyngio` with `createHtmlModule(...)`:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
```
|
||||||
|
|
||||||
|
Lyng code can then import it:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
val page = html {
|
||||||
|
head {
|
||||||
|
title { +"Demo" }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
nav {
|
||||||
|
a(href: "/") { +"Home" }
|
||||||
|
}
|
||||||
|
h3 { +"Heading 3" }
|
||||||
|
p {
|
||||||
|
attr("data-id", 123)
|
||||||
|
+"Text is escaped: <safe>"
|
||||||
|
}
|
||||||
|
img(src: "/logo.png", alt: "Logo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`html { ... }` returns a `String` beginning with `<!doctype html>`.
|
||||||
|
|
||||||
|
## Escaping
|
||||||
|
|
||||||
|
Text appended with unary `+` is HTML-escaped:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
html {
|
||||||
|
body {
|
||||||
|
p { +"Text & <more>" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
produces:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!doctype html><html><body><p>Text & <more></p></body></html>
|
||||||
|
```
|
||||||
|
|
||||||
|
Attribute values are escaped with HTML attribute rules:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
p {
|
||||||
|
attr("data-x", "\"quoted\" & <tag>")
|
||||||
|
+"content"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `raw(...)` only for trusted markup:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
div {
|
||||||
|
raw("<span>already escaped or trusted</span>")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tag Helpers
|
||||||
|
|
||||||
|
Current tag helpers cover common structural tags (`head`, `body`, `main`,
|
||||||
|
`section`, `article`, `header`, `footer`, `nav`, `div`, `span`, `p`), headings
|
||||||
|
(`h1` through `h6`), lists (`ul`, `ol`, `li`), and text/code tags (`strong`,
|
||||||
|
`em`, `code`, `pre`, `script`, `style`).
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
body {
|
||||||
|
main {
|
||||||
|
section {
|
||||||
|
h2 { +"News" }
|
||||||
|
p { +"First item" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Common void tags are also available: `meta`, `link`, `img`, `br`, and `input`.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
head {
|
||||||
|
meta { attr("charset", "utf-8") }
|
||||||
|
link {
|
||||||
|
attr("rel", "stylesheet")
|
||||||
|
attr("href", "/site.css")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
Use `attr(name, value)` inside a tag block to set an escaped attribute value.
|
||||||
|
`id(...)` and `classes(...)` are small aliases:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
div {
|
||||||
|
id("root")
|
||||||
|
classes("app shell")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `flag(name)` for boolean attributes:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
input {
|
||||||
|
attr("type", "checkbox")
|
||||||
|
flag("checked")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convenience Helpers
|
||||||
|
|
||||||
|
Convenience helpers include `metaCharset()`, `stylesheet(href)`,
|
||||||
|
`a(href) { ... }`, `img(src, alt)`, and `input(type, name, value)`.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
head {
|
||||||
|
metaCharset()
|
||||||
|
stylesheet("/site.css")
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
nav {
|
||||||
|
a(href: "/home") { +"Home" }
|
||||||
|
}
|
||||||
|
img(src: "/logo.png", alt: "Logo & mark")
|
||||||
|
input(type: "hidden", name: "token", value: "abc")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generic Elements
|
||||||
|
|
||||||
|
Use `tag(name) { ... }` and `voidTag(name) { ... }` for elements that do not
|
||||||
|
have dedicated helpers yet:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
body {
|
||||||
|
tag("custom-element") {
|
||||||
|
flag("hidden")
|
||||||
|
+"Secret"
|
||||||
|
}
|
||||||
|
voidTag("source") {
|
||||||
|
attr("srcset", "/image.webp")
|
||||||
|
attr("type", "image/webp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These helpers are intentionally simple escape hatches. Prefer a dedicated helper
|
||||||
|
when one exists because it can encode safer defaults and clearer parameter names.
|
||||||
@ -1,4 +1,4 @@
|
|||||||
### lyng.io.http — HTTP/HTTPS client for Lyng scripts
|
# lyng.io.http — HTTP/HTTPS client for Lyng scripts
|
||||||
|
|
||||||
This module provides a compact HTTP client API for Lyng scripts. It is implemented in `lyngio` and backed by Ktor on supported runtimes.
|
This module provides a compact HTTP client API for Lyng scripts. It is implemented in `lyngio` and backed by Ktor on supported runtimes.
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ This module provides a compact HTTP client API for Lyng scripts. It is implement
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Add the library to your project (Gradle)
|
## Add the library to your project (Gradle)
|
||||||
|
|
||||||
If you use this repository as a multi-module project, add a dependency on `:lyngio`:
|
If you use this repository as a multi-module project, add a dependency on `:lyngio`:
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ For external projects, ensure you also use the Lyng Maven repository described i
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Install the module into a Lyng session
|
## Install the module into a Lyng session
|
||||||
|
|
||||||
The HTTP module is not installed automatically. Install it into the session scope and provide a policy.
|
The HTTP module is not installed automatically. Install it into the session scope and provide a policy.
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ suspend fun bootstrapHttp() {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Using from Lyng scripts
|
## Using from Lyng scripts
|
||||||
|
|
||||||
Simple GET:
|
Simple GET:
|
||||||
|
|
||||||
@ -86,9 +86,9 @@ HTTPS GET:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### API reference
|
## API reference
|
||||||
|
|
||||||
##### `Http` (static methods)
|
### `Http` (static methods)
|
||||||
|
|
||||||
- `isSupported(): Bool` — Whether HTTP client support is available on the current runtime.
|
- `isSupported(): Bool` — Whether HTTP client support is available on the current runtime.
|
||||||
- `request(req: HttpRequest): HttpResponse` — Execute a request described by a mutable request object.
|
- `request(req: HttpRequest): HttpResponse` — Execute a request described by a mutable request object.
|
||||||
@ -101,7 +101,7 @@ For convenience methods, `headers...` accepts:
|
|||||||
- `MapEntry`, e.g. `"Accept" => "text/plain"`
|
- `MapEntry`, e.g. `"Accept" => "text/plain"`
|
||||||
- 2-item lists, e.g. `["Accept", "text/plain"]`
|
- 2-item lists, e.g. `["Accept", "text/plain"]`
|
||||||
|
|
||||||
##### `HttpRequest`
|
### `HttpRequest`
|
||||||
|
|
||||||
- `method: String`
|
- `method: String`
|
||||||
- `url: String`
|
- `url: String`
|
||||||
@ -112,7 +112,7 @@ For convenience methods, `headers...` accepts:
|
|||||||
|
|
||||||
Only one of `bodyText` and `bodyBytes` should be set.
|
Only one of `bodyText` and `bodyBytes` should be set.
|
||||||
|
|
||||||
##### `HttpResponse`
|
### `HttpResponse`
|
||||||
|
|
||||||
- `status: Int`
|
- `status: Int`
|
||||||
- `statusText: String`
|
- `statusText: String`
|
||||||
@ -122,7 +122,7 @@ Only one of `bodyText` and `bodyBytes` should be set.
|
|||||||
|
|
||||||
Response body decoding is cached inside the response object.
|
Response body decoding is cached inside the response object.
|
||||||
|
|
||||||
##### `HttpHeaders`
|
### `HttpHeaders`
|
||||||
|
|
||||||
`HttpHeaders` behaves like `Map<String, String>` for the first value of each header name and additionally exposes:
|
`HttpHeaders` behaves like `Map<String, String>` for the first value of each header name and additionally exposes:
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ Header lookup is case-insensitive.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Security policy
|
## Security policy
|
||||||
|
|
||||||
The module uses `HttpAccessPolicy` to authorize requests before they are sent.
|
The module uses `HttpAccessPolicy` to authorize requests before they are sent.
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ val allowLocalOnly = object : HttpAccessPolicy {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### Platform support
|
## Platform support
|
||||||
|
|
||||||
- **JVM:** supported
|
- **JVM:** supported
|
||||||
- **Android:** supported via the Ktor CIO client backend
|
- **Android:** supported via the Ktor CIO client backend
|
||||||
|
|||||||
@ -38,6 +38,7 @@ Route handlers use `RequestContext` as the receiver, so inside handlers you norm
|
|||||||
|
|
||||||
- `jsonBody<T>()`
|
- `jsonBody<T>()`
|
||||||
- `respondJson(...)`
|
- `respondJson(...)`
|
||||||
|
- `respondHtml { ... }`
|
||||||
- `respondText(...)`
|
- `respondText(...)`
|
||||||
- `setHeader(...)`
|
- `setHeader(...)`
|
||||||
- `request.path`
|
- `request.path`
|
||||||
@ -45,6 +46,41 @@ Route handlers use `RequestContext` as the receiver, so inside handlers you norm
|
|||||||
|
|
||||||
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda.
|
This keeps ordinary HTTP endpoints compact and avoids passing an explicit request or exchange parameter through every route lambda.
|
||||||
|
|
||||||
|
## HTML Response Sugar
|
||||||
|
|
||||||
|
Use `respondHtml { ... }` to render an HTML document with the `lyng.io.html` DSL and send it as `text/html; charset=utf-8`.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.io.http.server
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
val server = HttpServer()
|
||||||
|
|
||||||
|
server.get("/") {
|
||||||
|
respondHtml {
|
||||||
|
head {
|
||||||
|
title { +"Lyng status" }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
h3 { +"Service is running" }
|
||||||
|
p { +("Path: ${request.path}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.listen(8080, "127.0.0.1")
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass `code:` when the route should return a non-200 status:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
server.get("/accepted") {
|
||||||
|
respondHtml(code: 202) {
|
||||||
|
body { h3 { +"Accepted" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## JSON API Sugar
|
## JSON API Sugar
|
||||||
|
|
||||||
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
||||||
@ -54,25 +90,28 @@ For ordinary JSON APIs, `RequestContext` includes two primary helpers:
|
|||||||
|
|
||||||
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
These helpers intentionally use ordinary JSON projection for HTTP interop, not canonical `Json.encode(...)`.
|
||||||
|
|
||||||
### Typed JSON POST
|
### Typed JSON POST With Route Params
|
||||||
|
|
||||||
```lyng
|
```lyng
|
||||||
import lyng.io.http.server
|
import lyng.io.http.server
|
||||||
|
|
||||||
closed class CreateUserRequest(name: String, age: Int)
|
closed class CreateResultRequest(title: String, score: Int)
|
||||||
closed class CreateUserResponse(id: Int, name: String, age: Int)
|
closed class CreateResultResponse(id: String, userId: String, title: String, score: Int)
|
||||||
|
|
||||||
val server = HttpServer()
|
val server = HttpServer()
|
||||||
|
|
||||||
server.postPath("/api/users") {
|
server.postPath("/api/users/{userId}/results") {
|
||||||
val req = jsonBody<CreateUserRequest>()
|
val req = jsonBody<CreateResultRequest>()
|
||||||
|
|
||||||
if (req.name.isBlank()) {
|
if (req.title.isBlank()) {
|
||||||
respondJson({ error: "name must not be empty" }, 400)
|
respondJson({ error: "title must not be empty" }, 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
respondJson(
|
||||||
|
CreateResultResponse("r-101", routeParams["userId"], req.title, req.score),
|
||||||
|
201
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(8080, "127.0.0.1")
|
server.listen(8080, "127.0.0.1")
|
||||||
@ -118,6 +157,7 @@ server.listen(8080, "127.0.0.1")
|
|||||||
- `respond(...)`
|
- `respond(...)`
|
||||||
- `respondText(...)`
|
- `respondText(...)`
|
||||||
- `respondJson(body, status = 200)`
|
- `respondJson(body, status = 200)`
|
||||||
|
- `respondHtml(code: 200) { ... }`
|
||||||
- `setHeader(...)`
|
- `setHeader(...)`
|
||||||
- `addHeader(...)`
|
- `addHeader(...)`
|
||||||
- `acceptWebSocket(...)`
|
- `acceptWebSocket(...)`
|
||||||
|
|||||||
50
docs/samples/html_builder_dsl.lyng
Normal file
50
docs/samples/html_builder_dsl.lyng
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
class Tag(name: String) {
|
||||||
|
val name = name
|
||||||
|
var inner = ""
|
||||||
|
|
||||||
|
fun child(tagName: String, block: Tag.()->void) {
|
||||||
|
val child = Tag(tagName)
|
||||||
|
with(child) { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun head(block: Tag.()->void) { child("head", block) }
|
||||||
|
fun body(block: Tag.()->void) { child("body", block) }
|
||||||
|
fun title(block: Tag.()->void) { child("title", block) }
|
||||||
|
fun h1(block: Tag.()->void) { child("h1", block) }
|
||||||
|
|
||||||
|
fun addText(text: String) {
|
||||||
|
inner += text
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render() {
|
||||||
|
"<" + name + ">" + inner + "</" + name + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
this@Tag.addText(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun html(block: Tag.()->void) {
|
||||||
|
val root = Tag("html")
|
||||||
|
with(root) { block(this) }
|
||||||
|
root.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
val page = html {
|
||||||
|
head {
|
||||||
|
title {
|
||||||
|
+"Demo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
h1 {
|
||||||
|
+"Heading 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println(page)
|
||||||
|
assertEquals("<html><head><title>Demo</title></head><body><h1>Heading 1</h1></body></html>", page)
|
||||||
@ -1,6 +1,9 @@
|
|||||||
// Sample: Operator Overloading in Lyng
|
// Sample: Operator Overloading in Lyng
|
||||||
|
|
||||||
class Vector<T>(val x: T, val y: T) {
|
class Vector<T>(val x: T, val y: T) {
|
||||||
|
// Overload unary +
|
||||||
|
fun unaryPlus() = this
|
||||||
|
|
||||||
// Overload +
|
// Overload +
|
||||||
fun plus(other: Vector<U>) = Vector(x + other.x, y + other.y)
|
fun plus(other: Vector<U>) = Vector(x + other.x, y + other.y)
|
||||||
|
|
||||||
@ -28,6 +31,11 @@ val v2 = Vector(5, 5)
|
|||||||
println("v1: " + v1)
|
println("v1: " + v1)
|
||||||
println("v2: " + v2)
|
println("v2: " + v2)
|
||||||
|
|
||||||
|
// Test unary +
|
||||||
|
val v0 = +v1
|
||||||
|
println("+v1 = " + v0)
|
||||||
|
assertEquals(Vector(10, 20), v0)
|
||||||
|
|
||||||
// Test binary +
|
// Test binary +
|
||||||
val v3 = v1 + v2
|
val v3 = v1 + v2
|
||||||
println("v1 + v2 = " + v3)
|
println("v1 + v2 = " + v3)
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import net.sergeych.lyng.io.db.createDbModule
|
|||||||
import net.sergeych.lyng.io.db.jdbc.createJdbcModule
|
import net.sergeych.lyng.io.db.jdbc.createJdbcModule
|
||||||
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
import net.sergeych.lyng.io.db.sqlite.createSqliteModule
|
||||||
import net.sergeych.lyng.io.fs.createFs
|
import net.sergeych.lyng.io.fs.createFs
|
||||||
|
import net.sergeych.lyng.io.html.createHtmlModule
|
||||||
import net.sergeych.lyng.io.http.createHttpModule
|
import net.sergeych.lyng.io.http.createHttpModule
|
||||||
import net.sergeych.lyng.io.http.server.createHttpServerModule
|
import net.sergeych.lyng.io.http.server.createHttpServerModule
|
||||||
import net.sergeych.lyng.io.net.createNetModule
|
import net.sergeych.lyng.io.net.createNetModule
|
||||||
@ -146,6 +147,7 @@ private fun ImportManager.invalidateCliModuleCaches() {
|
|||||||
invalidatePackageCache("lyng.io.console")
|
invalidatePackageCache("lyng.io.console")
|
||||||
invalidatePackageCache("lyng.io.db.jdbc")
|
invalidatePackageCache("lyng.io.db.jdbc")
|
||||||
invalidatePackageCache("lyng.io.db.sqlite")
|
invalidatePackageCache("lyng.io.db.sqlite")
|
||||||
|
invalidatePackageCache("lyng.io.html")
|
||||||
invalidatePackageCache("lyng.io.http")
|
invalidatePackageCache("lyng.io.http")
|
||||||
invalidatePackageCache("lyng.io.http.server")
|
invalidatePackageCache("lyng.io.http.server")
|
||||||
invalidatePackageCache("lyng.io.ws")
|
invalidatePackageCache("lyng.io.ws")
|
||||||
@ -237,6 +239,7 @@ private fun installCliModules(manager: ImportManager) {
|
|||||||
createDbModule(manager)
|
createDbModule(manager)
|
||||||
createJdbcModule(manager)
|
createJdbcModule(manager)
|
||||||
createSqliteModule(manager)
|
createSqliteModule(manager)
|
||||||
|
createHtmlModule(manager)
|
||||||
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
createHttpModule(PermitAllHttpAccessPolicy, manager)
|
||||||
createHttpServerModule(PermitAllNetAccessPolicy, manager)
|
createHttpServerModule(PermitAllNetAccessPolicy, manager)
|
||||||
createWsModule(PermitAllWsAccessPolicy, manager)
|
createWsModule(PermitAllWsAccessPolicy, manager)
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.html
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyngio.stdlib_included.htmlLyng
|
||||||
|
|
||||||
|
private const val HTML_MODULE_NAME = "lyng.io.html"
|
||||||
|
|
||||||
|
fun createHtmlModule(scope: Scope): Boolean = createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
fun createHtml(scope: Scope): Boolean = createHtmlModule(scope)
|
||||||
|
|
||||||
|
fun createHtmlModule(manager: ImportManager): Boolean {
|
||||||
|
if (manager.packageNames.contains(HTML_MODULE_NAME)) return false
|
||||||
|
manager.addPackage(HTML_MODULE_NAME) { module ->
|
||||||
|
buildHtmlModule(module)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHtml(manager: ImportManager): Boolean = createHtmlModule(manager)
|
||||||
|
|
||||||
|
private suspend fun buildHtmlModule(module: ModuleScope) {
|
||||||
|
module.eval(Source(HTML_MODULE_NAME, htmlLyng))
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package net.sergeych.lyng.io.http.server
|
|||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import net.sergeych.lyng.ModuleScope
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.ScopeFacade
|
import net.sergeych.lyng.ScopeFacade
|
||||||
import net.sergeych.lyng.Source
|
import net.sergeych.lyng.Source
|
||||||
@ -28,6 +29,7 @@ import net.sergeych.lyng.obj.requiredArg
|
|||||||
import net.sergeych.lyng.obj.thisAs
|
import net.sergeych.lyng.obj.thisAs
|
||||||
import net.sergeych.lyng.io.http.ObjHttpHeaders
|
import net.sergeych.lyng.io.http.ObjHttpHeaders
|
||||||
import net.sergeych.lyng.io.http.createHttpTypesModule
|
import net.sergeych.lyng.io.http.createHttpTypesModule
|
||||||
|
import net.sergeych.lyng.io.html.createHtmlModule
|
||||||
import net.sergeych.lyng.io.ws.ObjWsMessage
|
import net.sergeych.lyng.io.ws.ObjWsMessage
|
||||||
import net.sergeych.lyng.io.ws.createWsTypesModule
|
import net.sergeych.lyng.io.ws.createWsTypesModule
|
||||||
import net.sergeych.lyng.serialization.ObjJsonClass
|
import net.sergeych.lyng.serialization.ObjJsonClass
|
||||||
@ -60,6 +62,7 @@ fun createHttpServer(policy: NetAccessPolicy, scope: Scope): Boolean = createHtt
|
|||||||
fun createHttpServerModule(policy: NetAccessPolicy, manager: ImportManager): Boolean {
|
fun createHttpServerModule(policy: NetAccessPolicy, manager: ImportManager): Boolean {
|
||||||
createHttpTypesModule(manager)
|
createHttpTypesModule(manager)
|
||||||
createWsTypesModule(manager)
|
createWsTypesModule(manager)
|
||||||
|
createHtmlModule(manager)
|
||||||
if (manager.packageNames.contains(HTTP_SERVER_MODULE_NAME)) return false
|
if (manager.packageNames.contains(HTTP_SERVER_MODULE_NAME)) return false
|
||||||
manager.addPackage(HTTP_SERVER_MODULE_NAME) { module ->
|
manager.addPackage(HTTP_SERVER_MODULE_NAME) { module ->
|
||||||
buildHttpServerModule(module, policy)
|
buildHttpServerModule(module, policy)
|
||||||
@ -119,6 +122,7 @@ private val regexType = TypeDecl.Simple("Regex", false)
|
|||||||
private val nullableRegexMatchType = TypeDecl.Simple("RegexMatch", true)
|
private val nullableRegexMatchType = TypeDecl.Simple("RegexMatch", true)
|
||||||
private val voidType = TypeDecl.Simple("Void", false)
|
private val voidType = TypeDecl.Simple("Void", false)
|
||||||
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
|
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
|
||||||
|
private val htmlTagType = TypeDecl.Simple("HtmlTag", false)
|
||||||
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
|
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
|
||||||
private val requestContextType = TypeDecl.Simple("RequestContext", false)
|
private val requestContextType = TypeDecl.Simple("RequestContext", false)
|
||||||
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
|
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
|
||||||
@ -876,6 +880,33 @@ private class ObjServerExchange(
|
|||||||
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
|
bridgeFn(
|
||||||
|
this,
|
||||||
|
"respondHtml",
|
||||||
|
base.getInstanceMemberOrNull("respondHtml")?.typeDecl as? TypeDecl.Function
|
||||||
|
?: fnType(voidType, intType, receiverFnType(htmlTagType, voidType)),
|
||||||
|
callSignature = receiverCallSignature("HtmlTag")
|
||||||
|
) {
|
||||||
|
val self = thisAs<ObjServerExchange>()
|
||||||
|
val first = args.list.getOrNull(0)
|
||||||
|
val second = args.list.getOrNull(1)
|
||||||
|
val status = args.named["code"]?.let { objToInt(this, it, "code") } ?: when {
|
||||||
|
first is ObjInt && second != null -> first.value.toInt()
|
||||||
|
second is ObjInt -> second.value.toInt()
|
||||||
|
else -> 200
|
||||||
|
}
|
||||||
|
val builder = when {
|
||||||
|
first is ObjInt -> second
|
||||||
|
else -> first
|
||||||
|
} ?: raiseIllegalArgument("respondHtml requires a builder")
|
||||||
|
val htmlModule = requireScope().importManager.createModuleScope(Pos.builtIn, "lyng.io.html")
|
||||||
|
val htmlFn = htmlModule.get("html")?.value ?: raiseIllegalState("lyng.io.html.html is not available")
|
||||||
|
val bodyText = (call(htmlFn, Arguments(listOf(builder))) as ObjString).value
|
||||||
|
self.ensureMutable(this)
|
||||||
|
self.responseHeaders["Content-Type"] = mutableListOf("text/html; charset=utf-8")
|
||||||
|
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
bridgeFn(this, "setHeader", fnType(voidType, stringType, stringType)) {
|
bridgeFn(this, "setHeader", fnType(voidType, stringType, stringType)) {
|
||||||
val self = thisAs<ObjServerExchange>()
|
val self = thisAs<ObjServerExchange>()
|
||||||
val name = requiredArg<ObjString>(0).value
|
val name = requiredArg<ObjString>(0).value
|
||||||
|
|||||||
@ -0,0 +1,196 @@
|
|||||||
|
/*
|
||||||
|
* 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.io.html
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class LyngHtmlModuleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testModuleRegistrationIsIdempotent() = runTest {
|
||||||
|
val importManager = ImportManager()
|
||||||
|
assertTrue(createHtmlModule(importManager))
|
||||||
|
assertFalse(createHtmlModule(importManager))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testModuleCanBeImported() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
val result = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<html-test>",
|
||||||
|
"""
|
||||||
|
import lyng.io.html
|
||||||
|
42
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
scope.importManager
|
||||||
|
).execute(scope)
|
||||||
|
|
||||||
|
assertEquals("42", result.inspect(scope))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHtmlDslBuildsNestedDocument() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
val result = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<html-dsl-test>",
|
||||||
|
"""
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
title { +"Demo" }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
h3 { +"Heading 3" }
|
||||||
|
p {
|
||||||
|
attr("data-x", "\"quoted\" & <tag>")
|
||||||
|
+"Text & <more>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
scope.importManager
|
||||||
|
).execute(scope)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"<!doctype html><html><head><title>Demo</title></head><body><h3>Heading 3</h3><p data-x=\""quoted" & <tag>\">Text & <more></p></body></html>",
|
||||||
|
(result as ObjString).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHtmlDslSupportsRawAndVoidTags() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
val result = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<html-void-test>",
|
||||||
|
"""
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
meta { attr("charset", "utf-8") }
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
div {
|
||||||
|
id("root")
|
||||||
|
classes("app shell")
|
||||||
|
raw("<span>trusted</span>")
|
||||||
|
br {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
scope.importManager
|
||||||
|
).execute(scope)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"<!doctype html><html><head><meta charset=\"utf-8\"></head><body><div id=\"root\" class=\"app shell\"><span>trusted</span><br></div></body></html>",
|
||||||
|
(result as ObjString).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHtmlDslTypedAttributeHelpers() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
val result = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<html-typed-attrs-test>",
|
||||||
|
"""
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
html {
|
||||||
|
head {
|
||||||
|
metaCharset()
|
||||||
|
stylesheet("/site.css")
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
nav {
|
||||||
|
a(href: "/home") { +"Home" }
|
||||||
|
}
|
||||||
|
img(src: "/logo.png", alt: "Logo & mark")
|
||||||
|
input(type: "hidden", name: "token", value: "\"abc\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
scope.importManager
|
||||||
|
).execute(scope)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"<!doctype html><html><head><meta charset=\"utf-8\"><link rel=\"stylesheet\" href=\"/site.css\"></head><body><nav><a href=\"/home\">Home</a></nav><img src=\"/logo.png\" alt=\"Logo & mark\"><input type=\"hidden\" name=\"token\" value=\""abc"\"></body></html>",
|
||||||
|
(result as ObjString).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHtmlDslGenericTagsAndFlagAttributes() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHtmlModule(scope.importManager)
|
||||||
|
|
||||||
|
val result = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<html-generic-tag-test>",
|
||||||
|
"""
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
html {
|
||||||
|
body {
|
||||||
|
tag("custom-element") {
|
||||||
|
flag("hidden")
|
||||||
|
+"Secret"
|
||||||
|
}
|
||||||
|
voidTag("source") {
|
||||||
|
attr("srcset", "/image.webp")
|
||||||
|
attr("type", "image/webp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
scope.importManager
|
||||||
|
).execute(scope)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"<!doctype html><html><body><custom-element hidden>Secret</custom-element><source srcset=\"/image.webp\" type=\"image/webp\"></body></html>",
|
||||||
|
(result as ObjString).value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -315,6 +315,53 @@ class LyngHttpServerModuleTest {
|
|||||||
handle.invokeInstanceMethod(scope, "close")
|
handle.invokeInstanceMethod(scope, "close")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun respondHtmlRendersHtmlDslAndSetsContentType() = runBlocking {
|
||||||
|
val engine = getSystemNetEngine()
|
||||||
|
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
||||||
|
|
||||||
|
val scope = Script.newScope()
|
||||||
|
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
||||||
|
|
||||||
|
val code = """
|
||||||
|
import lyng.io.http.server
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
|
val server = HttpServer()
|
||||||
|
server.getPath("/html/{name}") {
|
||||||
|
respondHtml(code: 202) {
|
||||||
|
head { title { +"Greeting" } }
|
||||||
|
body {
|
||||||
|
h3 { +("Hello, " + routeParams["name"]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.listen(0, "127.0.0.1")
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val handle = Compiler.compile(code).execute(scope)
|
||||||
|
val port = waitForPort(handle, scope)
|
||||||
|
|
||||||
|
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
||||||
|
try {
|
||||||
|
client.writeUtf8("GET /html/alice%26bob HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||||
|
client.flush()
|
||||||
|
val response = readHttpResponse(client)
|
||||||
|
assertTrue(response.contains("202"), response)
|
||||||
|
assertTrue(response.contains("Content-Type: text/html; charset=utf-8"), response)
|
||||||
|
assertTrue(
|
||||||
|
response.endsWith(
|
||||||
|
"<!doctype html><html><head><title>Greeting</title></head><body><h3>Hello, alice&bob</h3></body></html>"
|
||||||
|
),
|
||||||
|
response
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.invokeInstanceMethod(scope, "close")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun routerMountPreservesBuiltInRoutingSemantics() = runBlocking {
|
fun routerMountPreservesBuiltInRoutingSemantics() = runBlocking {
|
||||||
val engine = getSystemNetEngine()
|
val engine = getSystemNetEngine()
|
||||||
|
|||||||
142
lyngio/stdlib/lyng/io/html.lyng
Normal file
142
lyngio/stdlib/lyng/io/html.lyng
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package lyng.io.html
|
||||||
|
|
||||||
|
import lyng.stdlib
|
||||||
|
|
||||||
|
fun escapeHtml(text: String): String {
|
||||||
|
val amp: String = text.replace("&", "&")
|
||||||
|
val lt: String = amp.replace("<", "<")
|
||||||
|
lt.replace(">", ">")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun escapeHtmlAttr(text: String): String {
|
||||||
|
val escaped: String = escapeHtml(text)
|
||||||
|
val quoted: String = escaped.replace("\"", """)
|
||||||
|
quoted.replace("'", "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
class HtmlTag(name: String, isVoid: Bool = false) {
|
||||||
|
val name = name
|
||||||
|
val isVoid = isVoid
|
||||||
|
var attributes = ""
|
||||||
|
var inner = ""
|
||||||
|
|
||||||
|
fun attr(name: String, value: Object): HtmlTag {
|
||||||
|
attributes += " " + name + "=\"" + escapeHtmlAttr(value.toString()) + "\""
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flag(name: String): HtmlTag {
|
||||||
|
attributes += " " + name
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun id(value: String): HtmlTag = attr("id", value)
|
||||||
|
|
||||||
|
fun classes(value: String): HtmlTag = attr("class", value)
|
||||||
|
|
||||||
|
fun addText(text: String): void {
|
||||||
|
inner += escapeHtml(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun raw(html: String): void {
|
||||||
|
inner += html
|
||||||
|
}
|
||||||
|
|
||||||
|
fun child(tagName: String, block: HtmlTag.()->void): void {
|
||||||
|
val child = HtmlTag(tagName)
|
||||||
|
with(child) { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun voidChild(tagName: String, block: HtmlTag.()->void): void {
|
||||||
|
val child = HtmlTag(tagName, true)
|
||||||
|
with(child) { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tag(name: String, block: HtmlTag.()->void): void { child(name, block) }
|
||||||
|
|
||||||
|
fun voidTag(name: String, block: HtmlTag.()->void): void { voidChild(name, block) }
|
||||||
|
|
||||||
|
fun render(): String {
|
||||||
|
if (isVoid) "<" + name + attributes + ">"
|
||||||
|
else "<" + name + attributes + ">" + inner + "</" + name + ">"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun head(block: HtmlTag.()->void): void { child("head", block) }
|
||||||
|
fun body(block: HtmlTag.()->void): void { child("body", block) }
|
||||||
|
fun title(block: HtmlTag.()->void): void { child("title", block) }
|
||||||
|
fun main(block: HtmlTag.()->void): void { child("main", block) }
|
||||||
|
fun section(block: HtmlTag.()->void): void { child("section", block) }
|
||||||
|
fun article(block: HtmlTag.()->void): void { child("article", block) }
|
||||||
|
fun header(block: HtmlTag.()->void): void { child("header", block) }
|
||||||
|
fun footer(block: HtmlTag.()->void): void { child("footer", block) }
|
||||||
|
fun nav(block: HtmlTag.()->void): void { child("nav", block) }
|
||||||
|
fun div(block: HtmlTag.()->void): void { child("div", block) }
|
||||||
|
fun span(block: HtmlTag.()->void): void { child("span", block) }
|
||||||
|
fun p(block: HtmlTag.()->void): void { child("p", block) }
|
||||||
|
fun a(href: String, block: HtmlTag.()->void): void {
|
||||||
|
child("a") {
|
||||||
|
attr("href", href)
|
||||||
|
block(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun ul(block: HtmlTag.()->void): void { child("ul", block) }
|
||||||
|
fun ol(block: HtmlTag.()->void): void { child("ol", block) }
|
||||||
|
fun li(block: HtmlTag.()->void): void { child("li", block) }
|
||||||
|
fun h1(block: HtmlTag.()->void): void { child("h1", block) }
|
||||||
|
fun h2(block: HtmlTag.()->void): void { child("h2", block) }
|
||||||
|
fun h3(block: HtmlTag.()->void): void { child("h3", block) }
|
||||||
|
fun h4(block: HtmlTag.()->void): void { child("h4", block) }
|
||||||
|
fun h5(block: HtmlTag.()->void): void { child("h5", block) }
|
||||||
|
fun h6(block: HtmlTag.()->void): void { child("h6", block) }
|
||||||
|
fun strong(block: HtmlTag.()->void): void { child("strong", block) }
|
||||||
|
fun em(block: HtmlTag.()->void): void { child("em", block) }
|
||||||
|
fun code(block: HtmlTag.()->void): void { child("code", block) }
|
||||||
|
fun pre(block: HtmlTag.()->void): void { child("pre", block) }
|
||||||
|
fun script(block: HtmlTag.()->void): void { child("script", block) }
|
||||||
|
fun style(block: HtmlTag.()->void): void { child("style", block) }
|
||||||
|
|
||||||
|
fun meta(block: HtmlTag.()->void): void { voidChild("meta", block) }
|
||||||
|
fun link(block: HtmlTag.()->void): void { voidChild("link", block) }
|
||||||
|
fun img(block: HtmlTag.()->void): void { voidChild("img", block) }
|
||||||
|
fun br(block: HtmlTag.()->void): void { voidChild("br", block) }
|
||||||
|
fun input(block: HtmlTag.()->void): void { voidChild("input", block) }
|
||||||
|
|
||||||
|
fun metaCharset(charset: String = "utf-8"): void {
|
||||||
|
meta { attr("charset", charset) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stylesheet(href: String): void {
|
||||||
|
link {
|
||||||
|
attr("rel", "stylesheet")
|
||||||
|
attr("href", href)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun img(src: String, alt: String = ""): void {
|
||||||
|
voidChild("img") {
|
||||||
|
attr("src", src)
|
||||||
|
if (alt != "") attr("alt", alt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun input(type: String, name: String = "", value: String = ""): void {
|
||||||
|
voidChild("input") {
|
||||||
|
attr("type", type)
|
||||||
|
if (name != "") attr("name", name)
|
||||||
|
if (value != "") attr("value", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(HtmlTag)
|
||||||
|
fun String.unaryPlus(): void {
|
||||||
|
this@HtmlTag.addText(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun html(block: HtmlTag.()->void): String {
|
||||||
|
val root = HtmlTag("html")
|
||||||
|
with(root) { block(this) }
|
||||||
|
"<!doctype html>" + root.render()
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package lyng.io.http.server
|
|||||||
import lyng.io.http.types
|
import lyng.io.http.types
|
||||||
import lyng.serialization
|
import lyng.serialization
|
||||||
import lyng.io.ws.types
|
import lyng.io.ws.types
|
||||||
|
import lyng.io.html
|
||||||
|
|
||||||
/* Immutable parsed incoming server request. */
|
/* Immutable parsed incoming server request. */
|
||||||
extern class ServerRequest {
|
extern class ServerRequest {
|
||||||
@ -36,6 +37,7 @@ extern class RequestContext {
|
|||||||
fun respond(status: Int = 200, body: Buffer? = null): void
|
fun respond(status: Int = 200, body: Buffer? = null): void
|
||||||
fun respondText(status: Int = 200, bodyText: String = ""): void
|
fun respondText(status: Int = 200, bodyText: String = ""): void
|
||||||
fun respondJson(body: Object?, status: Int = 200): void
|
fun respondJson(body: Object?, status: Int = 200): void
|
||||||
|
fun respondHtml(code: Int = 200, builder: HtmlTag.()->void): void
|
||||||
fun setHeader(name: String, value: String): void
|
fun setHeader(name: String, value: String): void
|
||||||
fun addHeader(name: String, value: String): void
|
fun addHeader(name: String, value: String): void
|
||||||
fun acceptWebSocket(handler: RequestContext.(ServerWebSocket) -> Object?): void
|
fun acceptWebSocket(handler: RequestContext.(ServerWebSocket) -> Object?): void
|
||||||
|
|||||||
@ -853,7 +853,16 @@ class Compiler(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
if (name == null) return null
|
if (name == null) return null
|
||||||
val signature = callSignatureForName(name)
|
val signature = when (left) {
|
||||||
|
is ImplicitThisMemberRef -> {
|
||||||
|
val receiverType = left.preferredThisTypeName() ?: implicitReceiverTypeForMember(name)
|
||||||
|
receiverType
|
||||||
|
?.let { resolveClassByName(it) }
|
||||||
|
?.getInstanceMemberOrNull(name)
|
||||||
|
?.callSignature
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
} ?: callSignatureForName(name)
|
||||||
return signature?.tailBlockReceiverType ?: if (name == "flow") "FlowBuilder" else null
|
return signature?.tailBlockReceiverType ?: if (name == "flow") "FlowBuilder" else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -885,6 +894,19 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun promotePreferredReceiverArg(context: Scope, preferredTypeName: String?) {
|
||||||
|
if (preferredTypeName == null) return
|
||||||
|
val receiverArg = context.args.list.firstOrNull { arg ->
|
||||||
|
arg.isInstanceOf(preferredTypeName) ||
|
||||||
|
((context[preferredTypeName]?.value as? ObjClass)?.let { typeClass ->
|
||||||
|
arg.isInstanceOf(typeClass)
|
||||||
|
} == true)
|
||||||
|
} ?: return
|
||||||
|
if (context.thisVariants.firstOrNull() !== receiverArg) {
|
||||||
|
context.setThisVariants(receiverArg, context.thisVariants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun currentImplicitReceiverTypeNames(): List<String> {
|
private fun currentImplicitReceiverTypeNames(): List<String> {
|
||||||
val result = mutableListOf<String>()
|
val result = mutableListOf<String>()
|
||||||
for (ctx in codeContexts.asReversed()) {
|
for (ctx in codeContexts.asReversed()) {
|
||||||
@ -2010,6 +2032,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
callSignatureByName = callSignatureByName,
|
callSignatureByName = callSignatureByName,
|
||||||
|
extensionContextReceiversByWrapperName = extensionContextReceiversByWrapperName,
|
||||||
externBindingNames = externBindingNames,
|
externBindingNames = externBindingNames,
|
||||||
preparedModuleBindingNames = importBindings.keys,
|
preparedModuleBindingNames = importBindings.keys,
|
||||||
scopeRefPosByName = moduleReferencePosByName,
|
scopeRefPosByName = moduleReferencePosByName,
|
||||||
@ -2078,19 +2101,60 @@ class Compiler(
|
|||||||
private val rangeParamNamesStack = mutableListOf<Set<String>>()
|
private val rangeParamNamesStack = mutableListOf<Set<String>>()
|
||||||
private val extensionNames = mutableSetOf<String>()
|
private val extensionNames = mutableSetOf<String>()
|
||||||
private val extensionNamesByType = mutableMapOf<String, MutableSet<String>>()
|
private val extensionNamesByType = mutableMapOf<String, MutableSet<String>>()
|
||||||
|
private val extensionContextReceiversByWrapperName = mutableMapOf<String, List<String>>()
|
||||||
private val useScopeSlots: Boolean = seedScope == null
|
private val useScopeSlots: Boolean = seedScope == null
|
||||||
|
|
||||||
private fun registerExtensionName(typeName: String, memberName: String) {
|
private fun registerExtensionName(typeName: String, memberName: String) {
|
||||||
extensionNamesByType.getOrPut(typeName) { mutableSetOf() }.add(memberName)
|
extensionNamesByType.getOrPut(typeName) { mutableSetOf() }.add(memberName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun contextReceiverTypeName(typeDecl: TypeDecl): String? = when (typeDecl) {
|
||||||
|
is TypeDecl.Simple -> typeDecl.name.substringAfterLast('.')
|
||||||
|
is TypeDecl.Generic -> typeDecl.name.substringAfterLast('.')
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contextReceiversSatisfied(required: List<String>, visibleReceivers: List<String> = currentImplicitReceiverTypeNames()): Boolean {
|
||||||
|
if (required.isEmpty()) return true
|
||||||
|
return required.all { req ->
|
||||||
|
visibleReceivers.any { visible ->
|
||||||
|
visible == req || resolveClassByName(visible)?.let { cls ->
|
||||||
|
cls.className == req || cls.mro.any { it.className == req }
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rememberExtensionContextReceivers(wrapperName: String, record: ObjRecord) {
|
||||||
|
val fnType = record.typeDecl as? TypeDecl.Function ?: return
|
||||||
|
if (fnType.contextReceivers.isEmpty()) return
|
||||||
|
val names = fnType.contextReceivers.mapNotNull(::contextReceiverTypeName)
|
||||||
|
if (names.size == fnType.contextReceivers.size) {
|
||||||
|
extensionContextReceiversByWrapperName[wrapperName] = names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasExtensionFor(typeName: String, memberName: String): Boolean {
|
private fun hasExtensionFor(typeName: String, memberName: String): Boolean {
|
||||||
if (extensionNamesByType[typeName]?.contains(memberName) == true) return true
|
if (extensionNamesByType[typeName]?.contains(memberName) == true) {
|
||||||
|
val wrapperName = extensionCallableName(typeName, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[wrapperName].orEmpty())) return true
|
||||||
|
val getterName = extensionPropertyGetterName(typeName, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[getterName].orEmpty())) return true
|
||||||
|
val setterName = extensionPropertySetterName(typeName, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[setterName].orEmpty())) return true
|
||||||
|
}
|
||||||
val scopeRec = seedScope?.get(typeName) ?: importManager.rootScope.get(typeName)
|
val scopeRec = seedScope?.get(typeName) ?: importManager.rootScope.get(typeName)
|
||||||
val cls = (scopeRec?.value as? ObjClass) ?: resolveTypeDeclObjClass(TypeDecl.Simple(typeName, false))
|
val cls = (scopeRec?.value as? ObjClass) ?: resolveTypeDeclObjClass(TypeDecl.Simple(typeName, false))
|
||||||
if (cls != null) {
|
if (cls != null) {
|
||||||
for (base in cls.mro) {
|
for (base in cls.mro) {
|
||||||
if (extensionNamesByType[base.className]?.contains(memberName) == true) return true
|
if (extensionNamesByType[base.className]?.contains(memberName) == true) {
|
||||||
|
val wrapperName = extensionCallableName(base.className, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[wrapperName].orEmpty())) return true
|
||||||
|
val getterName = extensionPropertyGetterName(base.className, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[getterName].orEmpty())) return true
|
||||||
|
val setterName = extensionPropertySetterName(base.className, memberName)
|
||||||
|
if (contextReceiversSatisfied(extensionContextReceiversByWrapperName[setterName].orEmpty())) return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val candidates = mutableListOf(typeName)
|
val candidates = mutableListOf(typeName)
|
||||||
@ -2102,7 +2166,10 @@ class Compiler(
|
|||||||
extensionPropertySetterName(baseName, memberName)
|
extensionPropertySetterName(baseName, memberName)
|
||||||
)
|
)
|
||||||
for (wrapperName in wrapperNames) {
|
for (wrapperName in wrapperNames) {
|
||||||
|
if (!contextReceiversSatisfied(extensionContextReceiversByWrapperName[wrapperName].orEmpty())) continue
|
||||||
val resolved = resolveImportBinding(wrapperName, Pos.builtIn) ?: continue
|
val resolved = resolveImportBinding(wrapperName, Pos.builtIn) ?: continue
|
||||||
|
rememberExtensionContextReceivers(wrapperName, resolved.record)
|
||||||
|
if (!contextReceiversSatisfied(extensionContextReceiversByWrapperName[wrapperName].orEmpty())) continue
|
||||||
val plan = moduleSlotPlan()
|
val plan = moduleSlotPlan()
|
||||||
if (plan != null && !plan.slots.containsKey(wrapperName)) {
|
if (plan != null && !plan.slots.containsKey(wrapperName)) {
|
||||||
declareSlotNameIn(
|
declareSlotNameIn(
|
||||||
@ -2385,6 +2452,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
callSignatureByName = callSignatureByName,
|
callSignatureByName = callSignatureByName,
|
||||||
|
extensionContextReceiversByWrapperName = extensionContextReceiversByWrapperName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
externBindingNames = externBindingNames,
|
externBindingNames = externBindingNames,
|
||||||
preparedModuleBindingNames = importBindings.keys,
|
preparedModuleBindingNames = importBindings.keys,
|
||||||
@ -2420,6 +2488,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
callSignatureByName = callSignatureByName,
|
callSignatureByName = callSignatureByName,
|
||||||
|
extensionContextReceiversByWrapperName = extensionContextReceiversByWrapperName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
externBindingNames = externBindingNames,
|
externBindingNames = externBindingNames,
|
||||||
preparedModuleBindingNames = importBindings.keys,
|
preparedModuleBindingNames = importBindings.keys,
|
||||||
@ -2480,6 +2549,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
callSignatureByName = callSignatureByName,
|
callSignatureByName = callSignatureByName,
|
||||||
|
extensionContextReceiversByWrapperName = extensionContextReceiversByWrapperName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
externBindingNames = externBindingNames,
|
externBindingNames = externBindingNames,
|
||||||
preparedModuleBindingNames = importBindings.keys,
|
preparedModuleBindingNames = importBindings.keys,
|
||||||
@ -3721,7 +3791,7 @@ class Compiler(
|
|||||||
val inlineBodyRef = argsDeclaration?.let { null } ?: extractInlineLambdaBodyRef(body)
|
val inlineBodyRef = argsDeclaration?.let { null } ?: extractInlineLambdaBodyRef(body)
|
||||||
val supportsDirectInvokeFastPath = bytecodeFn != null &&
|
val supportsDirectInvokeFastPath = bytecodeFn != null &&
|
||||||
bytecodeFn.scopeSlotCount == 0 &&
|
bytecodeFn.scopeSlotCount == 0 &&
|
||||||
expectedReceiverType == null &&
|
effectiveExpectedReceiverType == null &&
|
||||||
!wrapAsExtensionCallable &&
|
!wrapAsExtensionCallable &&
|
||||||
!containsDelegatedRefs(body)
|
!containsDelegatedRefs(body)
|
||||||
val ref = LambdaFnRef(
|
val ref = LambdaFnRef(
|
||||||
@ -3733,9 +3803,10 @@ class Compiler(
|
|||||||
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
|
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement
|
||||||
|
|
||||||
override fun callOnFast(scope: Scope): Obj? {
|
override fun callOnFast(scope: Scope): Obj? {
|
||||||
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also {
|
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = effectiveExpectedReceiverType).also {
|
||||||
it.args = scope.args
|
it.args = scope.args
|
||||||
}
|
}
|
||||||
|
promotePreferredReceiverArg(context, effectiveExpectedReceiverType)
|
||||||
if (captureSlots.isNotEmpty()) {
|
if (captureSlots.isNotEmpty()) {
|
||||||
if (captureRecords != null) {
|
if (captureRecords != null) {
|
||||||
context.captureRecords = captureRecords
|
context.captureRecords = captureRecords
|
||||||
@ -3804,9 +3875,10 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun execute(scope: Scope): Obj {
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also {
|
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = effectiveExpectedReceiverType).also {
|
||||||
it.args = scope.args
|
it.args = scope.args
|
||||||
}
|
}
|
||||||
|
promotePreferredReceiverArg(context, effectiveExpectedReceiverType)
|
||||||
if (captureSlots.isNotEmpty()) {
|
if (captureSlots.isNotEmpty()) {
|
||||||
if (captureRecords != null) {
|
if (captureRecords != null) {
|
||||||
context.captureRecords = captureRecords
|
context.captureRecords = captureRecords
|
||||||
@ -5321,6 +5393,7 @@ class Compiler(
|
|||||||
is RangeRef -> ObjRange.type
|
is RangeRef -> ObjRange.type
|
||||||
is ClassOperatorRef -> ObjClassType
|
is ClassOperatorRef -> ObjClassType
|
||||||
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
|
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
|
||||||
|
is UnaryOpRef -> inferUnaryOpReturnClass(ref)
|
||||||
is IndexRef -> {
|
is IndexRef -> {
|
||||||
val targetClass = resolveReceiverClassForMember(ref.targetRef)
|
val targetClass = resolveReceiverClassForMember(ref.targetRef)
|
||||||
classMethodReturnClass(targetClass, "getAt")
|
classMethodReturnClass(targetClass, "getAt")
|
||||||
@ -5439,6 +5512,7 @@ class Compiler(
|
|||||||
?: resolveClassByName(ref.receiverTypeName())?.let { classMethodReturnTypeDecl(it, ref.methodName()) }
|
?: resolveClassByName(ref.receiverTypeName())?.let { classMethodReturnTypeDecl(it, ref.methodName()) }
|
||||||
}
|
}
|
||||||
is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref)
|
is CallRef -> callReturnTypeDeclByRef[ref] ?: inferCallReturnTypeDecl(ref)
|
||||||
|
is UnaryOpRef -> inferUnaryOpReturnTypeDecl(ref)
|
||||||
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
||||||
is ElvisRef -> inferElvisTypeDecl(ref)
|
is ElvisRef -> inferElvisTypeDecl(ref)
|
||||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||||
@ -5513,6 +5587,7 @@ class Compiler(
|
|||||||
is QualifiedThisMethodSlotCallRef ->
|
is QualifiedThisMethodSlotCallRef ->
|
||||||
inferMethodCallReturnClass(resolveClassByName(ref.receiverTypeName()), ref.methodName())
|
inferMethodCallReturnClass(resolveClassByName(ref.receiverTypeName()), ref.methodName())
|
||||||
is CallRef -> inferCallReturnTypeDecl(ref)?.let { resolveTypeDeclObjClass(it) } ?: inferCallReturnClass(ref)
|
is CallRef -> inferCallReturnTypeDecl(ref)?.let { resolveTypeDeclObjClass(it) } ?: inferCallReturnClass(ref)
|
||||||
|
is UnaryOpRef -> inferUnaryOpReturnClass(ref)
|
||||||
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||||
is FieldRef -> {
|
is FieldRef -> {
|
||||||
val targetClass = resolveReceiverClassForMember(ref.target)
|
val targetClass = resolveReceiverClassForMember(ref.target)
|
||||||
@ -5539,6 +5614,13 @@ class Compiler(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun unaryOpMethodName(op: UnaryOp): String? = when (op) {
|
||||||
|
UnaryOp.POSITIVE -> "unaryPlus"
|
||||||
|
UnaryOp.NEGATE -> "negate"
|
||||||
|
UnaryOp.BITNOT -> "bitNot"
|
||||||
|
UnaryOp.NOT -> null
|
||||||
|
}
|
||||||
|
|
||||||
private fun interopOperatorFor(op: BinOp): InteropOperator? = when (op) {
|
private fun interopOperatorFor(op: BinOp): InteropOperator? = when (op) {
|
||||||
BinOp.PLUS -> InteropOperator.Plus
|
BinOp.PLUS -> InteropOperator.Plus
|
||||||
BinOp.MINUS -> InteropOperator.Minus
|
BinOp.MINUS -> InteropOperator.Minus
|
||||||
@ -5614,6 +5696,67 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferExtensionMethodReturnTypeDecl(
|
||||||
|
receiverDecl: TypeDecl?,
|
||||||
|
receiverClass: ObjClass?,
|
||||||
|
memberName: String
|
||||||
|
): TypeDecl? {
|
||||||
|
if (receiverClass == null) return null
|
||||||
|
for (cls in receiverClass.mro) {
|
||||||
|
val wrapperName = extensionCallableName(cls.className, memberName)
|
||||||
|
val resolved = resolveImportBinding(wrapperName, Pos.builtIn) ?: continue
|
||||||
|
registerImportBinding(wrapperName, resolved.binding, Pos.builtIn)
|
||||||
|
val wrapperType = resolved.record.typeDecl as? TypeDecl.Function ?: continue
|
||||||
|
val bindings = mutableMapOf<String, TypeDecl>()
|
||||||
|
val receiverParam = wrapperType.params.firstOrNull() ?: wrapperType.receiver
|
||||||
|
if (receiverParam != null && receiverDecl != null) {
|
||||||
|
collectTypeVarBindings(receiverParam, receiverDecl, bindings)
|
||||||
|
}
|
||||||
|
return if (bindings.isEmpty()) wrapperType.returnType
|
||||||
|
else substituteTypeAliasTypeVars(wrapperType.returnType, bindings)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferUnaryOpReturnTypeDecl(ref: UnaryOpRef): TypeDecl? {
|
||||||
|
val operandDecl = resolveReceiverTypeDecl(ref.a)
|
||||||
|
val operandClass = resolveReceiverClassForMember(ref.a) ?: inferObjClassFromRef(ref.a)
|
||||||
|
return when (ref.op) {
|
||||||
|
UnaryOp.NOT -> typeDeclOfClass(ObjBool.type)
|
||||||
|
UnaryOp.POSITIVE -> {
|
||||||
|
unaryOpMethodName(ref.op)?.let { methodName ->
|
||||||
|
classMethodReturnTypeDecl(operandClass, methodName)?.let { return it }
|
||||||
|
inferExtensionMethodReturnTypeDecl(operandDecl, operandClass, methodName)?.let { return it }
|
||||||
|
}
|
||||||
|
operandDecl ?: operandClass?.let(::typeDeclOfClass)
|
||||||
|
}
|
||||||
|
UnaryOp.NEGATE -> when (operandClass) {
|
||||||
|
ObjInt.type -> typeDeclOfClass(ObjInt.type)
|
||||||
|
ObjReal.type -> typeDeclOfClass(ObjReal.type)
|
||||||
|
else -> unaryOpMethodName(ref.op)?.let { methodName ->
|
||||||
|
classMethodReturnTypeDecl(operandClass, methodName)
|
||||||
|
?: inferExtensionMethodReturnTypeDecl(operandDecl, operandClass, methodName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UnaryOp.BITNOT -> when (operandClass) {
|
||||||
|
ObjInt.type -> typeDeclOfClass(ObjInt.type)
|
||||||
|
else -> unaryOpMethodName(ref.op)?.let { methodName ->
|
||||||
|
classMethodReturnTypeDecl(operandClass, methodName)
|
||||||
|
?: inferExtensionMethodReturnTypeDecl(operandDecl, operandClass, methodName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferUnaryOpReturnClass(ref: UnaryOpRef): ObjClass? {
|
||||||
|
inferUnaryOpReturnTypeDecl(ref)?.let { declared ->
|
||||||
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
|
if (declared is TypeDecl.TypeVar) return Obj.rootObjectType
|
||||||
|
}
|
||||||
|
val operandClass = resolveReceiverClassForMember(ref.a) ?: inferObjClassFromRef(ref.a)
|
||||||
|
return if (ref.op == UnaryOp.POSITIVE) operandClass else null
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
||||||
inferBinaryOpReturnTypeDecl(ref)?.let { declared ->
|
inferBinaryOpReturnTypeDecl(ref)?.let { declared ->
|
||||||
resolveTypeDeclObjClass(declared)?.let { return it }
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
@ -7439,6 +7582,7 @@ class Compiler(
|
|||||||
is FastLocalVarRef -> nameObjClass[ref.name]?.className
|
is FastLocalVarRef -> nameObjClass[ref.name]?.className
|
||||||
?: nameTypeDecl[ref.name]?.let { typeDeclName(it) }
|
?: nameTypeDecl[ref.name]?.let { typeDeclName(it) }
|
||||||
is QualifiedThisRef -> ref.typeName
|
is QualifiedThisRef -> ref.typeName
|
||||||
|
is UnaryOpRef -> inferUnaryOpReturnClass(ref)?.className
|
||||||
else -> resolveReceiverClassForMember(ref)?.className
|
else -> resolveReceiverClassForMember(ref)?.className
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7458,8 +7602,12 @@ class Compiler(
|
|||||||
Token.Type.CHAR -> ConstRef(ObjChar(t.value[0]).asReadonly)
|
Token.Type.CHAR -> ConstRef(ObjChar(t.value[0]).asReadonly)
|
||||||
|
|
||||||
Token.Type.PLUS -> {
|
Token.Type.PLUS -> {
|
||||||
val n = parseNumber(true)
|
parseNumberOrNull(true)?.let { n ->
|
||||||
ConstRef(n.asReadonly)
|
ConstRef(n.asReadonly)
|
||||||
|
} ?: run {
|
||||||
|
val n = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression after unary plus")
|
||||||
|
UnaryOpRef(UnaryOp.POSITIVE, n)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.MINUS -> {
|
Token.Type.MINUS -> {
|
||||||
@ -7655,6 +7803,43 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseContextReceiverDeclarationList(start: Pos): List<TypeDecl> {
|
||||||
|
if (!cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
|
||||||
|
throw ScriptError(start, "expected '(' after context")
|
||||||
|
}
|
||||||
|
val receivers = mutableListOf<TypeDecl>()
|
||||||
|
cc.skipWsTokens()
|
||||||
|
if (cc.peekNextNonWhitespace().type == Token.Type.RPAREN) {
|
||||||
|
cc.nextNonWhitespace()
|
||||||
|
return receivers
|
||||||
|
}
|
||||||
|
while (true) {
|
||||||
|
val (decl, _) = parseTypeExpressionWithMini()
|
||||||
|
receivers += decl
|
||||||
|
val sep = cc.nextNonWhitespace()
|
||||||
|
when (sep.type) {
|
||||||
|
Token.Type.COMMA -> continue
|
||||||
|
Token.Type.RPAREN -> return receivers
|
||||||
|
else -> sep.raiseSyntax("expected ',' or ')' in context receiver list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun parseContextFunctionDeclaration(contextToken: Token): Statement {
|
||||||
|
val contextReceivers = parseContextReceiverDeclarationList(contextToken.pos)
|
||||||
|
val fn = cc.nextNonWhitespace()
|
||||||
|
if (fn.type != Token.Type.ID || (fn.value != "fun" && fn.value != "fn")) {
|
||||||
|
throw ScriptError(fn.pos, "context receivers are currently supported only on function declarations")
|
||||||
|
}
|
||||||
|
pendingDeclStart = contextToken.pos
|
||||||
|
pendingDeclDoc = consumePendingDoc()
|
||||||
|
return parseFunctionDeclaration(
|
||||||
|
isExtern = false,
|
||||||
|
isStatic = false,
|
||||||
|
contextReceiverTypeDecls = contextReceivers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse keyword-starting statement.
|
* Parse keyword-starting statement.
|
||||||
* @return parsed statement or null if, for example. [id] is not among keywords
|
* @return parsed statement or null if, for example. [id] is not among keywords
|
||||||
@ -7693,6 +7878,7 @@ class Compiler(
|
|||||||
pendingDeclDoc = consumePendingDoc()
|
pendingDeclDoc = consumePendingDoc()
|
||||||
parseFunctionDeclaration(isExtern = false, isStatic = false)
|
parseFunctionDeclaration(isExtern = false, isStatic = false)
|
||||||
}
|
}
|
||||||
|
"context" -> parseContextFunctionDeclaration(id)
|
||||||
// Visibility modifiers for declarations: private/protected val/var/fun/fn
|
// Visibility modifiers for declarations: private/protected val/var/fun/fn
|
||||||
"while" -> parseWhileStatement()
|
"while" -> parseWhileStatement()
|
||||||
"do" -> parseDoWhileStatement()
|
"do" -> parseDoWhileStatement()
|
||||||
@ -9648,7 +9834,8 @@ class Compiler(
|
|||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
isExtern: Boolean = false,
|
isExtern: Boolean = false,
|
||||||
isStatic: Boolean = false,
|
isStatic: Boolean = false,
|
||||||
isTransient: Boolean = isTransientFlag
|
isTransient: Boolean = isTransientFlag,
|
||||||
|
contextReceiverTypeDecls: List<TypeDecl> = emptyList()
|
||||||
): Statement {
|
): Statement {
|
||||||
isTransientFlag = false
|
isTransientFlag = false
|
||||||
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
|
val declarationAnnotationSpecs = pendingDeclAnnotations.toList()
|
||||||
@ -9688,7 +9875,17 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
registerExtensionName(extTypeName, name)
|
registerExtensionName(extTypeName, name)
|
||||||
|
if (contextReceiverTypeDecls.isNotEmpty()) {
|
||||||
|
val contextNames = contextReceiverTypeDecls.mapNotNull(::contextReceiverTypeName)
|
||||||
|
if (contextNames.size != contextReceiverTypeDecls.size) {
|
||||||
|
throw ScriptError(start, "context receiver types for extension functions must be class-like")
|
||||||
|
}
|
||||||
|
extensionContextReceiversByWrapperName[extensionCallableName(extTypeName, name)] = contextNames
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (contextReceiverTypeDecls.isNotEmpty()) {
|
||||||
|
throw ScriptError(start, "context receivers are currently supported only on extension functions")
|
||||||
|
}
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
if (t.type != Token.Type.ID)
|
if (t.type != Token.Type.ID)
|
||||||
throw ScriptError(t.pos, "Expected identifier after 'fun'")
|
throw ScriptError(t.pos, "Expected identifier after 'fun'")
|
||||||
@ -9774,6 +9971,7 @@ class Compiler(
|
|||||||
if (parentContext is CodeContext.ClassBody && !isStatic && extTypeName == null) {
|
if (parentContext is CodeContext.ClassBody && !isStatic && extTypeName == null) {
|
||||||
classMemberTypeDeclByName.getOrPut(parentContext.name) { mutableMapOf() }[name] = TypeDecl.Function(
|
classMemberTypeDeclByName.getOrPut(parentContext.name) { mutableMapOf() }[name] = TypeDecl.Function(
|
||||||
receiver = receiverTypeDecl,
|
receiver = receiverTypeDecl,
|
||||||
|
contextReceivers = contextReceiverTypeDecls,
|
||||||
params = argsDeclaration.params.map { it.type },
|
params = argsDeclaration.params.map { it.type },
|
||||||
returnType = returnTypeDecl ?: TypeDecl.TypeAny,
|
returnType = returnTypeDecl ?: TypeDecl.TypeAny,
|
||||||
nullable = false
|
nullable = false
|
||||||
@ -9851,7 +10049,7 @@ class Compiler(
|
|||||||
CodeContext.Function(
|
CodeContext.Function(
|
||||||
name,
|
name,
|
||||||
implicitThisMembers = implicitThisMembers,
|
implicitThisMembers = implicitThisMembers,
|
||||||
implicitReceiverTypeNames = listOfNotNull(implicitThisTypeName),
|
implicitReceiverTypeNames = listOfNotNull(implicitThisTypeName) + contextReceiverTypeDecls.mapNotNull(::contextReceiverTypeName),
|
||||||
typeParams = typeParams,
|
typeParams = typeParams,
|
||||||
typeParamDecls = typeParamDecls,
|
typeParamDecls = typeParamDecls,
|
||||||
noImplicitThis = noImplicitThis
|
noImplicitThis = noImplicitThis
|
||||||
@ -9949,6 +10147,7 @@ class Compiler(
|
|||||||
run {
|
run {
|
||||||
val memberTypeDecl = TypeDecl.Function(
|
val memberTypeDecl = TypeDecl.Function(
|
||||||
receiver = receiverTypeDecl,
|
receiver = receiverTypeDecl,
|
||||||
|
contextReceivers = contextReceiverTypeDecls,
|
||||||
params = argsDeclaration.params.map { it.type },
|
params = argsDeclaration.params.map { it.type },
|
||||||
returnType = inferredReturnDecl ?: TypeDecl.TypeAny,
|
returnType = inferredReturnDecl ?: TypeDecl.TypeAny,
|
||||||
nullable = false
|
nullable = false
|
||||||
@ -10062,7 +10261,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (extTypeName != null) {
|
if (extTypeName != null) {
|
||||||
context.thisObj = scope.thisObj
|
context.setThisVariants(scope.thisObj, context.thisVariants)
|
||||||
}
|
}
|
||||||
val localNames = frame.fn.localSlotNames
|
val localNames = frame.fn.localSlotNames
|
||||||
for (i in localNames.indices) {
|
for (i in localNames.indices) {
|
||||||
@ -10145,6 +10344,7 @@ class Compiler(
|
|||||||
annotation = annotation,
|
annotation = annotation,
|
||||||
typeDecl = if (isDelegated) null else TypeDecl.Function(
|
typeDecl = if (isDelegated) null else TypeDecl.Function(
|
||||||
receiver = receiverTypeDecl,
|
receiver = receiverTypeDecl,
|
||||||
|
contextReceivers = contextReceiverTypeDecls,
|
||||||
params = argsDeclaration.params.map { it.type },
|
params = argsDeclaration.params.map { it.type },
|
||||||
returnType = inferredReturnDecl ?: TypeDecl.TypeAny,
|
returnType = inferredReturnDecl ?: TypeDecl.TypeAny,
|
||||||
nullable = false
|
nullable = false
|
||||||
@ -11644,6 +11844,7 @@ class Compiler(
|
|||||||
val a = constOf(aRef) ?: return null
|
val a = constOf(aRef) ?: return null
|
||||||
return when (op) {
|
return when (op) {
|
||||||
UnaryOp.NOT -> if (a is ObjBool) if (!a.value) ObjTrue else ObjFalse else null
|
UnaryOp.NOT -> if (a is ObjBool) if (!a.value) ObjTrue else ObjFalse else null
|
||||||
|
UnaryOp.POSITIVE -> a
|
||||||
UnaryOp.NEGATE -> when (a) {
|
UnaryOp.NEGATE -> when (a) {
|
||||||
is ObjInt -> ObjInt.of(-a.value)
|
is ObjInt -> ObjInt.of(-a.value)
|
||||||
is ObjReal -> ObjReal.of(-a.value)
|
is ObjReal -> ObjReal.of(-a.value)
|
||||||
|
|||||||
@ -99,6 +99,20 @@ open class Scope(
|
|||||||
extensions.getOrPut(cls) { mutableMapOf() }[name] = record
|
extensions.getOrPut(cls) { mutableMapOf() }[name] = record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extensionContextReceiversSatisfied(record: ObjRecord): Boolean {
|
||||||
|
val fnType = record.typeDecl as? TypeDecl.Function ?: return true
|
||||||
|
if (fnType.contextReceivers.isEmpty()) return true
|
||||||
|
return fnType.contextReceivers.all { required ->
|
||||||
|
thisVariants.any { variant ->
|
||||||
|
when (required) {
|
||||||
|
is TypeDecl.Simple -> variant.isInstanceOf(required.name.substringAfterLast('.'))
|
||||||
|
is TypeDecl.Generic -> variant.isInstanceOf(required.name.substringAfterLast('.'))
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
||||||
var s: Scope? = this
|
var s: Scope? = this
|
||||||
var hops = 0
|
var hops = 0
|
||||||
@ -106,7 +120,9 @@ open class Scope(
|
|||||||
// Proximity rule: check all extensions in the current scope before going to parent.
|
// Proximity rule: check all extensions in the current scope before going to parent.
|
||||||
// Priority within scope: more specific class in MRO wins.
|
// Priority within scope: more specific class in MRO wins.
|
||||||
for (cls in receiverClass.mro) {
|
for (cls in receiverClass.mro) {
|
||||||
s.extensions[cls]?.get(name)?.let { return it }
|
s.extensions[cls]?.get(name)?.let {
|
||||||
|
if (extensionContextReceiversSatisfied(it)) return it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (s is BytecodeClosureScope) {
|
if (s is BytecodeClosureScope) {
|
||||||
s.closureScope.findExtension(receiverClass, name)?.let { return it }
|
s.closureScope.findExtension(receiverClass, name)?.let { return it }
|
||||||
|
|||||||
@ -43,6 +43,7 @@ class BytecodeCompiler(
|
|||||||
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||||
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||||
private val callSignatureByName: Map<String, CallSignature> = emptyMap(),
|
private val callSignatureByName: Map<String, CallSignature> = emptyMap(),
|
||||||
|
private val extensionContextReceiversByWrapperName: Map<String, List<String>> = emptyMap(),
|
||||||
private val externCallableNames: Set<String> = emptySet(),
|
private val externCallableNames: Set<String> = emptySet(),
|
||||||
private val externBindingNames: Set<String> = emptySet(),
|
private val externBindingNames: Set<String> = emptySet(),
|
||||||
private val preparedModuleBindingNames: Set<String> = emptySet(),
|
private val preparedModuleBindingNames: Set<String> = emptySet(),
|
||||||
@ -1146,56 +1147,95 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileUnary(ref: UnaryOpRef): CompiledValue? {
|
private fun compileUnary(ref: UnaryOpRef): CompiledValue? {
|
||||||
val a = compileRef(unaryOperand(ref)) ?: return null
|
|
||||||
val out = allocSlot()
|
|
||||||
return when (unaryOp(ref)) {
|
return when (unaryOp(ref)) {
|
||||||
UnaryOp.NEGATE -> when (a.type) {
|
UnaryOp.POSITIVE -> {
|
||||||
SlotType.INT -> {
|
val operandRef = unaryOperand(ref)
|
||||||
builder.emit(Opcode.NEG_INT, a.slot, out)
|
if (hasUnaryCallable(operandRef, "unaryPlus")) {
|
||||||
CompiledValue(out, SlotType.INT)
|
return compileMethodCall(MethodCallRef(operandRef, "unaryPlus", emptyList(), false, false))
|
||||||
}
|
}
|
||||||
SlotType.REAL -> {
|
val a = compileRef(operandRef) ?: return null
|
||||||
builder.emit(Opcode.NEG_REAL, a.slot, out)
|
return when (a.type) {
|
||||||
CompiledValue(out, SlotType.REAL)
|
SlotType.INT, SlotType.REAL -> a
|
||||||
}
|
else -> {
|
||||||
else -> compileObjUnaryOp(unaryOperand(ref), a, "negate", Pos.builtIn)
|
val obj = ensureObjSlot(a)
|
||||||
}
|
val out = allocSlot()
|
||||||
UnaryOp.NOT -> {
|
builder.emit(Opcode.POS_OBJ, obj.slot, out)
|
||||||
when (a.type) {
|
updateSlotType(out, SlotType.OBJ)
|
||||||
SlotType.BOOL -> builder.emit(Opcode.NOT_BOOL, a.slot, out)
|
slotObjClass[obj.slot]?.let { slotObjClass[out] = it }
|
||||||
SlotType.INT -> {
|
CompiledValue(out, SlotType.OBJ)
|
||||||
val tmp = allocSlot()
|
|
||||||
builder.emit(Opcode.INT_TO_BOOL, a.slot, tmp)
|
|
||||||
builder.emit(Opcode.NOT_BOOL, tmp, out)
|
|
||||||
}
|
}
|
||||||
SlotType.OBJ, SlotType.UNKNOWN -> {
|
|
||||||
val objSlot = ensureObjSlot(a)
|
|
||||||
val tmp = allocSlot()
|
|
||||||
builder.emit(Opcode.OBJ_TO_BOOL, objSlot.slot, tmp)
|
|
||||||
builder.emit(Opcode.NOT_BOOL, tmp, out)
|
|
||||||
updateSlotType(tmp, SlotType.BOOL)
|
|
||||||
}
|
|
||||||
else -> return null
|
|
||||||
}
|
}
|
||||||
CompiledValue(out, SlotType.BOOL)
|
|
||||||
}
|
}
|
||||||
UnaryOp.BITNOT -> {
|
else -> {
|
||||||
if (a.type == SlotType.INT) {
|
val a = compileRef(unaryOperand(ref)) ?: return null
|
||||||
builder.emit(Opcode.INV_INT, a.slot, out)
|
val out = allocSlot()
|
||||||
return CompiledValue(out, SlotType.INT)
|
when (unaryOp(ref)) {
|
||||||
|
UnaryOp.NEGATE -> when (a.type) {
|
||||||
|
SlotType.INT -> {
|
||||||
|
builder.emit(Opcode.NEG_INT, a.slot, out)
|
||||||
|
CompiledValue(out, SlotType.INT)
|
||||||
|
}
|
||||||
|
SlotType.REAL -> {
|
||||||
|
builder.emit(Opcode.NEG_REAL, a.slot, out)
|
||||||
|
CompiledValue(out, SlotType.REAL)
|
||||||
|
}
|
||||||
|
else -> compileObjUnaryOp(unaryOperand(ref), a, "negate", Pos.builtIn)
|
||||||
|
}
|
||||||
|
UnaryOp.NOT -> {
|
||||||
|
when (a.type) {
|
||||||
|
SlotType.BOOL -> builder.emit(Opcode.NOT_BOOL, a.slot, out)
|
||||||
|
SlotType.INT -> {
|
||||||
|
val tmp = allocSlot()
|
||||||
|
builder.emit(Opcode.INT_TO_BOOL, a.slot, tmp)
|
||||||
|
builder.emit(Opcode.NOT_BOOL, tmp, out)
|
||||||
|
}
|
||||||
|
SlotType.OBJ, SlotType.UNKNOWN -> {
|
||||||
|
val objSlot = ensureObjSlot(a)
|
||||||
|
val tmp = allocSlot()
|
||||||
|
builder.emit(Opcode.OBJ_TO_BOOL, objSlot.slot, tmp)
|
||||||
|
builder.emit(Opcode.NOT_BOOL, tmp, out)
|
||||||
|
updateSlotType(tmp, SlotType.BOOL)
|
||||||
|
}
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
CompiledValue(out, SlotType.BOOL)
|
||||||
|
}
|
||||||
|
UnaryOp.BITNOT -> {
|
||||||
|
if (a.type == SlotType.INT) {
|
||||||
|
builder.emit(Opcode.INV_INT, a.slot, out)
|
||||||
|
return CompiledValue(out, SlotType.INT)
|
||||||
|
}
|
||||||
|
return compileObjUnaryOp(unaryOperand(ref), a, "bitNot", Pos.builtIn)
|
||||||
|
}
|
||||||
|
UnaryOp.POSITIVE -> error("unreachable")
|
||||||
}
|
}
|
||||||
return compileObjUnaryOp(unaryOperand(ref), a, "bitNot", Pos.builtIn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasUnaryCallable(ref: ObjRef, memberName: String): Boolean {
|
||||||
|
val receiverClass = resolveReceiverClass(ref) ?: return false
|
||||||
|
if (receiverClass == ObjDynamic.type) return false
|
||||||
|
if (receiverClass is ObjInstanceClass && !isThisReceiver(ref)) return true
|
||||||
|
val resolvedMember = receiverClass.resolveInstanceMember(memberName)
|
||||||
|
if (resolvedMember?.declaringClass?.className == "Obj") return false
|
||||||
|
val abstractRecord = receiverClass.members[memberName] ?: receiverClass.classScope?.objects?.get(memberName)
|
||||||
|
if (abstractRecord?.isAbstract == true) return false
|
||||||
|
val methodId = receiverClass.instanceMethodIdMap(includeAbstract = true)[memberName]
|
||||||
|
if (methodId != null && resolvedMember?.declaringClass?.className != "Obj") return true
|
||||||
|
val fieldId = if (resolvedMember != null) receiverClass.instanceFieldIdMap()[memberName] else null
|
||||||
|
if (fieldId != null) return true
|
||||||
|
return resolveExtensionCallableSlot(receiverClass, memberName) != null
|
||||||
|
}
|
||||||
|
|
||||||
private fun compileObjUnaryOp(
|
private fun compileObjUnaryOp(
|
||||||
ref: ObjRef,
|
ref: ObjRef,
|
||||||
value: CompiledValue,
|
value: CompiledValue,
|
||||||
memberName: String,
|
memberName: String,
|
||||||
pos: Pos
|
pos: Pos,
|
||||||
|
defaultIdentity: Boolean = false
|
||||||
): CompiledValue? {
|
): CompiledValue? {
|
||||||
val receiverClass = resolveReceiverClass(ref)
|
val receiverClass = resolveReceiverClass(ref) ?: slotObjClass[value.slot]
|
||||||
val methodId = receiverClass?.instanceMethodIdMap(includeAbstract = true)?.get(memberName)
|
val methodId = receiverClass?.instanceMethodIdMap(includeAbstract = true)?.get(memberName)
|
||||||
if (methodId != null) {
|
if (methodId != null) {
|
||||||
val receiverObj = ensureObjSlot(value)
|
val receiverObj = ensureObjSlot(value)
|
||||||
@ -1204,6 +1244,19 @@ class BytecodeCompiler(
|
|||||||
updateSlotType(dst, SlotType.OBJ)
|
updateSlotType(dst, SlotType.OBJ)
|
||||||
return CompiledValue(dst, SlotType.OBJ)
|
return CompiledValue(dst, SlotType.OBJ)
|
||||||
}
|
}
|
||||||
|
val extSlot = when {
|
||||||
|
receiverClass != null -> resolveExtensionCallableSlot(receiverClass, memberName)
|
||||||
|
else -> resolveUniqueExtensionWrapperSlot(memberName, "__ext__")
|
||||||
|
}
|
||||||
|
if (extSlot != null) {
|
||||||
|
val callee = ensureObjSlot(extSlot)
|
||||||
|
val args = compileCallArgsWithReceiver(value, emptyList(), false) ?: return null
|
||||||
|
val encodedCount = encodeCallArgCount(args) ?: return null
|
||||||
|
val dst = allocSlot()
|
||||||
|
setPos(pos)
|
||||||
|
emitCallCompiled(callee, args.base, encodedCount, dst)
|
||||||
|
return CompiledValue(dst, SlotType.OBJ)
|
||||||
|
}
|
||||||
if (memberName == "negate" &&
|
if (memberName == "negate" &&
|
||||||
(receiverClass == null || isDelegateClass(receiverClass) || receiverClass in setOf(ObjInt.type, ObjReal.type))
|
(receiverClass == null || isDelegateClass(receiverClass) || receiverClass in setOf(ObjInt.type, ObjReal.type))
|
||||||
) {
|
) {
|
||||||
@ -1217,6 +1270,9 @@ class BytecodeCompiler(
|
|||||||
updateSlotType(dst, SlotType.OBJ)
|
updateSlotType(dst, SlotType.OBJ)
|
||||||
return CompiledValue(dst, SlotType.OBJ)
|
return CompiledValue(dst, SlotType.OBJ)
|
||||||
}
|
}
|
||||||
|
if (defaultIdentity) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
throw BytecodeCompileException(
|
throw BytecodeCompileException(
|
||||||
"Unknown member $memberName on ${receiverClass?.className ?: "unknown"}",
|
"Unknown member $memberName on ${receiverClass?.className ?: "unknown"}",
|
||||||
pos
|
pos
|
||||||
@ -5972,6 +6028,7 @@ class BytecodeCompiler(
|
|||||||
): String? {
|
): String? {
|
||||||
for (receiverName in extensionReceiverTypeNames(receiverClass)) {
|
for (receiverName in extensionReceiverTypeNames(receiverClass)) {
|
||||||
val candidate = wrapperName(receiverName, memberName)
|
val candidate = wrapperName(receiverName, memberName)
|
||||||
|
if (!extensionContextReceiversSatisfied(candidate)) continue
|
||||||
if (allowedScopeNames != null &&
|
if (allowedScopeNames != null &&
|
||||||
!allowedScopeNames.contains(candidate) &&
|
!allowedScopeNames.contains(candidate) &&
|
||||||
!localSlotIndexByName.containsKey(candidate)
|
!localSlotIndexByName.containsKey(candidate)
|
||||||
@ -5983,6 +6040,31 @@ class BytecodeCompiler(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun currentImplicitReceiverTypeNames(): List<String> {
|
||||||
|
val result = mutableListOf<String>()
|
||||||
|
inlineThisBindings.asReversed().forEach { binding ->
|
||||||
|
val typeName = binding.typeName ?: return@forEach
|
||||||
|
if (!result.contains(typeName)) result += typeName
|
||||||
|
}
|
||||||
|
implicitThisTypeName?.let {
|
||||||
|
if (!result.contains(it)) result += it
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extensionContextReceiversSatisfied(wrapperName: String): Boolean {
|
||||||
|
val required = extensionContextReceiversByWrapperName[wrapperName].orEmpty()
|
||||||
|
if (required.isEmpty()) return true
|
||||||
|
val visible = currentImplicitReceiverTypeNames()
|
||||||
|
return required.all { req ->
|
||||||
|
visible.any { visibleName ->
|
||||||
|
visibleName == req || resolveTypeNameClass(visibleName)?.let { cls ->
|
||||||
|
cls.className == req || cls.mro.any { it.className == req }
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveUniqueExtensionWrapperName(
|
private fun resolveUniqueExtensionWrapperName(
|
||||||
memberName: String,
|
memberName: String,
|
||||||
wrapperPrefix: String
|
wrapperPrefix: String
|
||||||
@ -5991,12 +6073,12 @@ class BytecodeCompiler(
|
|||||||
val candidates = LinkedHashSet<String>()
|
val candidates = LinkedHashSet<String>()
|
||||||
for (name in localSlotIndexByName.keys) {
|
for (name in localSlotIndexByName.keys) {
|
||||||
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
|
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
|
||||||
candidates.add(name)
|
if (extensionContextReceiversSatisfied(name)) candidates.add(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (name in scopeSlotIndexByName.keys) {
|
for (name in scopeSlotIndexByName.keys) {
|
||||||
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
|
if (name.startsWith(wrapperPrefix) && name.endsWith(suffix)) {
|
||||||
candidates.add(name)
|
if (extensionContextReceiversSatisfied(name)) candidates.add(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return candidates.singleOrNull()
|
return candidates.singleOrNull()
|
||||||
@ -8313,6 +8395,19 @@ class BytecodeCompiler(
|
|||||||
is ObjChar -> ObjChar.type
|
is ObjChar -> ObjChar.type
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
is UnaryOpRef -> when (ref.op) {
|
||||||
|
UnaryOp.NOT -> ObjBool.type
|
||||||
|
UnaryOp.POSITIVE -> resolveReceiverClass(ref.a)
|
||||||
|
UnaryOp.NEGATE -> when (val operandClass = resolveReceiverClass(ref.a)) {
|
||||||
|
ObjInt.type -> ObjInt.type
|
||||||
|
ObjReal.type -> ObjReal.type
|
||||||
|
else -> inferMethodCallReturnClass(operandClass, "negate")
|
||||||
|
}
|
||||||
|
UnaryOp.BITNOT -> when (val operandClass = resolveReceiverClass(ref.a)) {
|
||||||
|
ObjInt.type -> ObjInt.type
|
||||||
|
else -> inferMethodCallReturnClass(operandClass, "bitNot")
|
||||||
|
}
|
||||||
|
}
|
||||||
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
|
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
|
||||||
?: resolveReceiverClass(ref.castValueRef())
|
?: resolveReceiverClass(ref.castValueRef())
|
||||||
is FieldRef -> {
|
is FieldRef -> {
|
||||||
|
|||||||
@ -107,6 +107,7 @@ class BytecodeStatement private constructor(
|
|||||||
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||||
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||||
callSignatureByName: Map<String, CallSignature> = emptyMap(),
|
callSignatureByName: Map<String, CallSignature> = emptyMap(),
|
||||||
|
extensionContextReceiversByWrapperName: Map<String, List<String>> = emptyMap(),
|
||||||
externCallableNames: Set<String> = emptySet(),
|
externCallableNames: Set<String> = emptySet(),
|
||||||
externBindingNames: Set<String> = emptySet(),
|
externBindingNames: Set<String> = emptySet(),
|
||||||
preparedModuleBindingNames: Set<String> = emptySet(),
|
preparedModuleBindingNames: Set<String> = emptySet(),
|
||||||
@ -148,6 +149,7 @@ class BytecodeStatement private constructor(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
callSignatureByName = callSignatureByName,
|
callSignatureByName = callSignatureByName,
|
||||||
|
extensionContextReceiversByWrapperName = extensionContextReceiversByWrapperName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
externBindingNames = externBindingNames,
|
externBindingNames = externBindingNames,
|
||||||
preparedModuleBindingNames = preparedModuleBindingNames,
|
preparedModuleBindingNames = preparedModuleBindingNames,
|
||||||
|
|||||||
@ -143,7 +143,7 @@ class CmdBuilder {
|
|||||||
Opcode.UNBOX_INT_OBJ, Opcode.UNBOX_REAL_OBJ,
|
Opcode.UNBOX_INT_OBJ, Opcode.UNBOX_REAL_OBJ,
|
||||||
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
|
Opcode.INT_TO_REAL, Opcode.REAL_TO_INT, Opcode.BOOL_TO_INT, Opcode.INT_TO_BOOL,
|
||||||
Opcode.OBJ_TO_BOOL, Opcode.GET_OBJ_CLASS,
|
Opcode.OBJ_TO_BOOL, Opcode.GET_OBJ_CLASS,
|
||||||
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT,
|
Opcode.NEG_INT, Opcode.NEG_REAL, Opcode.NOT_BOOL, Opcode.INV_INT, Opcode.POS_OBJ,
|
||||||
Opcode.ASSERT_IS ->
|
Opcode.ASSERT_IS ->
|
||||||
listOf(OperandKind.SLOT, OperandKind.SLOT)
|
listOf(OperandKind.SLOT, OperandKind.SLOT)
|
||||||
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
|
Opcode.CHECK_IS, Opcode.MAKE_QUALIFIED_VIEW ->
|
||||||
@ -698,6 +698,7 @@ class CmdBuilder {
|
|||||||
} else {
|
} else {
|
||||||
CmdNotBool(operands[0], operands[1])
|
CmdNotBool(operands[0], operands[1])
|
||||||
}
|
}
|
||||||
|
Opcode.POS_OBJ -> CmdPosObj(operands[0], operands[1])
|
||||||
Opcode.AND_BOOL -> if (isFastLocal(operands[0]) && isFastLocal(operands[1]) && isFastLocal(operands[2])) {
|
Opcode.AND_BOOL -> if (isFastLocal(operands[0]) && isFastLocal(operands[1]) && isFastLocal(operands[2])) {
|
||||||
CmdAndBoolLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2] - scopeSlotCount)
|
CmdAndBoolLocal(operands[0] - scopeSlotCount, operands[1] - scopeSlotCount, operands[2] - scopeSlotCount)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -450,6 +450,7 @@ object CmdDisassembler {
|
|||||||
is CmdMulObj -> Opcode.MUL_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
is CmdMulObj -> Opcode.MUL_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
||||||
is CmdDivObj -> Opcode.DIV_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
is CmdDivObj -> Opcode.DIV_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
||||||
is CmdModObj -> Opcode.MOD_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
is CmdModObj -> Opcode.MOD_OBJ to intArrayOf(cmd.a, cmd.b, cmd.dst)
|
||||||
|
is CmdPosObj -> Opcode.POS_OBJ to intArrayOf(cmd.a, cmd.dst)
|
||||||
is CmdContainsObj -> Opcode.CONTAINS_OBJ to intArrayOf(cmd.target, cmd.value, cmd.dst)
|
is CmdContainsObj -> Opcode.CONTAINS_OBJ to intArrayOf(cmd.target, cmd.value, cmd.dst)
|
||||||
is CmdAssignOpObj -> Opcode.ASSIGN_OP_OBJ to intArrayOf(cmd.opId, cmd.targetSlot, cmd.valueSlot, cmd.dst, cmd.nameId)
|
is CmdAssignOpObj -> Opcode.ASSIGN_OP_OBJ to intArrayOf(cmd.opId, cmd.targetSlot, cmd.valueSlot, cmd.dst, cmd.nameId)
|
||||||
is CmdJmp -> Opcode.JMP to intArrayOf(cmd.target)
|
is CmdJmp -> Opcode.JMP to intArrayOf(cmd.target)
|
||||||
@ -593,6 +594,8 @@ object CmdDisassembler {
|
|||||||
Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.CONTAINS_OBJ,
|
Opcode.ADD_OBJ, Opcode.SUB_OBJ, Opcode.MUL_OBJ, Opcode.DIV_OBJ, Opcode.MOD_OBJ, Opcode.CONTAINS_OBJ,
|
||||||
Opcode.AND_BOOL, Opcode.OR_BOOL ->
|
Opcode.AND_BOOL, Opcode.OR_BOOL ->
|
||||||
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
listOf(OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT)
|
||||||
|
Opcode.POS_OBJ ->
|
||||||
|
listOf(OperandKind.SLOT, OperandKind.SLOT)
|
||||||
Opcode.ASSIGN_OP_OBJ ->
|
Opcode.ASSIGN_OP_OBJ ->
|
||||||
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.CONST)
|
listOf(OperandKind.ID, OperandKind.SLOT, OperandKind.SLOT, OperandKind.SLOT, OperandKind.CONST)
|
||||||
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET, Opcode.ITER_PUSH, Opcode.LOAD_THIS ->
|
Opcode.INC_INT, Opcode.DEC_INT, Opcode.RET, Opcode.ITER_PUSH, Opcode.LOAD_THIS ->
|
||||||
|
|||||||
@ -1942,6 +1942,14 @@ class CmdCmpGteObj(internal val a: Int, internal val b: Int, internal val dst: I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CmdPosObj(internal val a: Int, internal val dst: Int) : Cmd() {
|
||||||
|
override suspend fun perform(frame: CmdFrame) {
|
||||||
|
val result = frame.slotToObj(a).unaryPlus(frame.ensureScope())
|
||||||
|
frame.storeObjResult(dst, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CmdAddObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
|
class CmdAddObj(internal val a: Int, internal val b: Int, internal val dst: Int) : Cmd() {
|
||||||
override suspend fun perform(frame: CmdFrame) {
|
override suspend fun perform(frame: CmdFrame) {
|
||||||
val result = frame.slotToObj(a).plus(frame.ensureScope(), frame.slotToObj(b))
|
val result = frame.slotToObj(a).plus(frame.ensureScope(), frame.slotToObj(b))
|
||||||
@ -4176,6 +4184,15 @@ class BytecodeLambdaCallable(
|
|||||||
val context = callScope.applyClosureForBytecode(closureScope, preferredThisType).also {
|
val context = callScope.applyClosureForBytecode(closureScope, preferredThisType).also {
|
||||||
it.args = args
|
it.args = args
|
||||||
}
|
}
|
||||||
|
preferredThisType?.let { typeName ->
|
||||||
|
val receiverArg = args.list.firstOrNull { arg ->
|
||||||
|
arg.isInstanceOf(typeName) ||
|
||||||
|
((context[typeName]?.value as? ObjClass)?.let { typeClass -> arg.isInstanceOf(typeClass) } == true)
|
||||||
|
}
|
||||||
|
if (receiverArg != null && context.thisVariants.firstOrNull() !== receiverArg) {
|
||||||
|
context.setThisVariants(receiverArg, context.thisVariants)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (captureRecords != null) {
|
if (captureRecords != null) {
|
||||||
context.captureRecords = captureRecords
|
context.captureRecords = captureRecords
|
||||||
context.captureNames = captureNames
|
context.captureNames = captureNames
|
||||||
|
|||||||
@ -144,6 +144,7 @@ enum class Opcode(val code: Int) {
|
|||||||
MOD_OBJ(0x7B),
|
MOD_OBJ(0x7B),
|
||||||
CONTAINS_OBJ(0x7C),
|
CONTAINS_OBJ(0x7C),
|
||||||
ASSIGN_OP_OBJ(0x7D),
|
ASSIGN_OP_OBJ(0x7D),
|
||||||
|
POS_OBJ(0x7E),
|
||||||
|
|
||||||
JMP(0x80),
|
JMP(0x80),
|
||||||
JMP_IF_TRUE(0x81),
|
JMP_IF_TRUE(0x81),
|
||||||
|
|||||||
@ -317,6 +317,12 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open suspend fun unaryPlus(scope: Scope): Obj {
|
||||||
|
return invokeInstanceMethod(scope, "unaryPlus", Arguments.EMPTY) {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open suspend fun mul(scope: Scope, other: Obj): Obj {
|
open suspend fun mul(scope: Scope, other: Obj): Obj {
|
||||||
val otherValue = when (other) {
|
val otherValue = when (other) {
|
||||||
is FrameSlotRef -> other.read()
|
is FrameSlotRef -> other.read()
|
||||||
|
|||||||
@ -73,7 +73,7 @@ class ClassOperatorRef(val target: ObjRef, val pos: Pos) : ObjRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Unary operations supported by ObjRef. */
|
/** Unary operations supported by ObjRef. */
|
||||||
enum class UnaryOp { NOT, NEGATE, BITNOT }
|
enum class UnaryOp { NOT, POSITIVE, NEGATE, BITNOT }
|
||||||
|
|
||||||
/** Binary operations supported by ObjRef. */
|
/** Binary operations supported by ObjRef. */
|
||||||
enum class BinOp {
|
enum class BinOp {
|
||||||
|
|||||||
@ -15,17 +15,21 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import net.sergeych.lyng.eval as lyngEval
|
import net.sergeych.lyng.eval as lyngEval
|
||||||
|
|
||||||
class LaunchPoolTest {
|
class LaunchPoolTest {
|
||||||
|
|
||||||
private suspend fun eval(code: String) = withTimeout(2_000L) { lyngEval(code) }
|
private suspend fun eval(code: String) = withContext(Dispatchers.Default) {
|
||||||
|
withTimeout(2_000L) { lyngEval(code) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBasicExecution() = runBlocking<Unit> {
|
fun testBasicExecution() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
val d1 = pool.launch { 1 + 1 }
|
val d1 = pool.launch { 1 + 1 }
|
||||||
@ -37,7 +41,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testResultsCollected() = runBlocking<Unit> {
|
fun testResultsCollected() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(4)
|
val pool = LaunchPool(4)
|
||||||
val jobs = (1..10).map { n -> pool.launch { n * n } }
|
val jobs = (1..10).map { n -> pool.launch { n * n } }
|
||||||
@ -48,7 +52,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testConcurrencyLimit() = runBlocking<Unit> {
|
fun testConcurrencyLimit() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
// With maxWorkers=2, at most 2 tasks run at the same time.
|
// With maxWorkers=2, at most 2 tasks run at the same time.
|
||||||
val mu = Mutex()
|
val mu = Mutex()
|
||||||
@ -70,7 +74,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testExceptionCapturedInDeferred() = runBlocking<Unit> {
|
fun testExceptionCapturedInDeferred() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
val good = pool.launch { 42 }
|
val good = pool.launch { 42 }
|
||||||
@ -83,7 +87,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testPoolContinuesAfterLambdaException() = runBlocking<Unit> {
|
fun testPoolContinuesAfterLambdaException() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(1)
|
val pool = LaunchPool(1)
|
||||||
val bad = pool.launch { throw IllegalArgumentException("fail") }
|
val bad = pool.launch { throw IllegalArgumentException("fail") }
|
||||||
@ -96,7 +100,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchAfterCloseAndJoinThrows() = runBlocking<Unit> {
|
fun testLaunchAfterCloseAndJoinThrows() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
pool.launch { 1 }
|
pool.launch { 1 }
|
||||||
@ -107,7 +111,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLaunchAfterCancelThrows() = runBlocking<Unit> {
|
fun testLaunchAfterCancelThrows() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
pool.cancel()
|
pool.cancel()
|
||||||
@ -117,7 +121,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCancelAndJoinWaitsForWorkers() = runBlocking<Unit> {
|
fun testCancelAndJoinWaitsForWorkers() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
pool.launch { delay(5) }
|
pool.launch { delay(5) }
|
||||||
@ -128,7 +132,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCloseAndJoinDrainsQueue() = runBlocking<Unit> {
|
fun testCloseAndJoinDrainsQueue() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val mu = Mutex()
|
val mu = Mutex()
|
||||||
val results = []
|
val results = []
|
||||||
@ -147,7 +151,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testBoundedQueueSuspendsProducer() = runBlocking<Unit> {
|
fun testBoundedQueueSuspendsProducer() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
// queue of 2 + 1 worker; producer can only be 1 ahead of what's running
|
// queue of 2 + 1 worker; producer can only be 1 ahead of what's running
|
||||||
val pool = LaunchPool(1, 2)
|
val pool = LaunchPool(1, 2)
|
||||||
@ -165,7 +169,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testUnlimitedQueueDefault() = runBlocking<Unit> {
|
fun testUnlimitedQueueDefault() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(4)
|
val pool = LaunchPool(4)
|
||||||
val jobs = (1..50).map { n -> pool.launch { n } }
|
val jobs = (1..50).map { n -> pool.launch { n } }
|
||||||
@ -177,7 +181,7 @@ class LaunchPoolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIdempotentClose() = runBlocking<Unit> {
|
fun testIdempotentClose() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
val pool = LaunchPool(2)
|
val pool = LaunchPool(2)
|
||||||
pool.closeAndJoin()
|
pool.closeAndJoin()
|
||||||
|
|||||||
@ -190,4 +190,63 @@ class ScriptImportPreparationTest {
|
|||||||
session.cancelAndJoin()
|
session.cancelAndJoin()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun importedContextReceiverExtensionIsAvailableInReceiverDsl() = runTest {
|
||||||
|
val manager = Script.defaultImportManager.copy().apply {
|
||||||
|
addTextPackages(
|
||||||
|
"""
|
||||||
|
package imported.ctxdsl
|
||||||
|
|
||||||
|
class Tag(name: String) {
|
||||||
|
val name = name
|
||||||
|
var inner = ""
|
||||||
|
|
||||||
|
fun child(tagName: String, block: Tag.()->void) {
|
||||||
|
val child = Tag(tagName)
|
||||||
|
child.apply { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun h3(block: Tag.()->void) { child("h3", block) }
|
||||||
|
fun addText(text: String) { inner += text }
|
||||||
|
fun render() = "<" + name + ">" + inner + "</" + name + ">"
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
this@Tag.addText(this)
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val script = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<ctx-dsl-import>",
|
||||||
|
"""
|
||||||
|
import imported.ctxdsl
|
||||||
|
|
||||||
|
fun html(block: Tag.()->void) {
|
||||||
|
val root = Tag("html")
|
||||||
|
root.apply { block(this) }
|
||||||
|
root.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
val page = html {
|
||||||
|
h3 {
|
||||||
|
+"Imported"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("<html><h3>Imported</h3></html>", page)
|
||||||
|
assertEquals("plain", +"plain")
|
||||||
|
page
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = script.execute(manager.newStdScope()) as ObjString
|
||||||
|
assertEquals("<html><h3>Imported</h3></html>", result.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.lyng.eval
|
import net.sergeych.lyng.eval
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ class TypeInferenceTest {
|
|||||||
|
|
||||||
/** Channel field type inferred from constructor — accessed in a launch closure */
|
/** Channel field type inferred from constructor — accessed in a launch closure */
|
||||||
@Test
|
@Test
|
||||||
fun testChannelFieldInLaunchClosure() = runBlocking<Unit> {
|
fun testChannelFieldInLaunchClosure() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Foo {
|
class Foo {
|
||||||
private val ch = Channel(Channel.UNLIMITED)
|
private val ch = Channel(Channel.UNLIMITED)
|
||||||
@ -52,7 +52,7 @@ class TypeInferenceTest {
|
|||||||
|
|
||||||
/** Mutex field type inferred from constructor — used directly in a method body */
|
/** Mutex field type inferred from constructor — used directly in a method body */
|
||||||
@Test
|
@Test
|
||||||
fun testMutexFieldDirectUse() = runBlocking<Unit> {
|
fun testMutexFieldDirectUse() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Bar {
|
class Bar {
|
||||||
private val mu = Mutex()
|
private val mu = Mutex()
|
||||||
@ -69,7 +69,7 @@ class TypeInferenceTest {
|
|||||||
|
|
||||||
/** CompletableDeferred field type inferred — complete/await used directly */
|
/** CompletableDeferred field type inferred — complete/await used directly */
|
||||||
@Test
|
@Test
|
||||||
fun testCompletableDeferredFieldDirectUse() = runBlocking<Unit> {
|
fun testCompletableDeferredFieldDirectUse() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Baz {
|
class Baz {
|
||||||
private val d = CompletableDeferred()
|
private val d = CompletableDeferred()
|
||||||
@ -84,7 +84,7 @@ class TypeInferenceTest {
|
|||||||
|
|
||||||
/** Channel field accessed inside a map closure within class initializer */
|
/** Channel field accessed inside a map closure within class initializer */
|
||||||
@Test
|
@Test
|
||||||
fun testChannelFieldInMapAndLaunchClosure() = runBlocking<Unit> {
|
fun testChannelFieldInMapAndLaunchClosure() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Pool(n) {
|
class Pool(n) {
|
||||||
private val ch = Channel(Channel.UNLIMITED)
|
private val ch = Channel(Channel.UNLIMITED)
|
||||||
@ -106,7 +106,7 @@ class TypeInferenceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runBlocking<Unit> {
|
fun testIterableFirstPreservesElementTypeForBlockReturnInference() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
class Item(title: String)
|
class Item(title: String)
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ class TypeInferenceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runBlocking<Unit> {
|
fun testCallableLocalInitializedFromFunctionCallPreservesReturnType() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
fun makeAdder(base) {
|
fun makeAdder(base) {
|
||||||
return { x -> x + base + 0.5 }
|
return { x -> x + base + 0.5 }
|
||||||
|
|||||||
@ -53,6 +53,197 @@ class OperatorOverloadingTest {
|
|||||||
""".trimIndent())
|
""".trimIndent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnaryPlusDefaultIdentity() = runTest {
|
||||||
|
eval("""
|
||||||
|
assertEquals(42, +42)
|
||||||
|
assertEquals(3.5, +3.5)
|
||||||
|
assertEquals("abc", +"abc")
|
||||||
|
|
||||||
|
class Box(val text: String) {
|
||||||
|
fun upper() = text.upper()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("ABC", (+Box("abc")).upper())
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnaryPlusOverloading() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Counter(val n: Int) {
|
||||||
|
fun unaryPlus() = Counter(this.n + 1)
|
||||||
|
fun equals(other: Counter) = this.n == other.n
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(Counter(6), Counter(5).unaryPlus())
|
||||||
|
assertEquals(Counter(6), +Counter(5))
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnaryPlusExtensionOverloading() = runTest {
|
||||||
|
eval("""
|
||||||
|
var out = ""
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
out = out + this
|
||||||
|
}
|
||||||
|
|
||||||
|
"Hello".unaryPlus()
|
||||||
|
" ".unaryPlus()
|
||||||
|
"Lyng".unaryPlus()
|
||||||
|
assertEquals("Hello Lyng", out)
|
||||||
|
out = ""
|
||||||
|
|
||||||
|
+"Hello"
|
||||||
|
+" "
|
||||||
|
+"Lyng"
|
||||||
|
assertEquals("Hello Lyng", out)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testUnaryPlusDslBuilderStyle() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Tag(name: String) {
|
||||||
|
val name = name
|
||||||
|
var inner = ""
|
||||||
|
|
||||||
|
fun child(tagName: String, block: Tag.()->void) {
|
||||||
|
val child = Tag(tagName)
|
||||||
|
with(child) { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun head(block: Tag.()->void) { child("head", block) }
|
||||||
|
fun body(block: Tag.()->void) { child("body", block) }
|
||||||
|
fun title(block: Tag.()->void) { child("title", block) }
|
||||||
|
fun h1(block: Tag.()->void) { child("h1", block) }
|
||||||
|
|
||||||
|
fun addText(text: String) {
|
||||||
|
inner += text
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render() {
|
||||||
|
"<" + name + ">" + inner + "</" + name + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
this@Tag.addText(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun html(block: Tag.()->void) {
|
||||||
|
val root = Tag("html")
|
||||||
|
with(root) { block(this) }
|
||||||
|
root.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
val page = html {
|
||||||
|
head {
|
||||||
|
title {
|
||||||
|
+"Demo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
h1 {
|
||||||
|
+"Heading 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("<html><head><title>Demo</title></head><body><h1>Heading 1</h1></body></html>", page)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContextReceiverUnaryPlusDslBuilderStyle() = runTest {
|
||||||
|
eval("""
|
||||||
|
class Tag(name: String) {
|
||||||
|
val name = name
|
||||||
|
var inner = ""
|
||||||
|
|
||||||
|
fun child(tagName: String, block: Tag.()->void) {
|
||||||
|
val child = Tag(tagName)
|
||||||
|
with(child) { block(this) }
|
||||||
|
inner += child.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun h3(block: Tag.()->void) { child("h3", block) }
|
||||||
|
|
||||||
|
fun addText(text: String) {
|
||||||
|
inner += text
|
||||||
|
}
|
||||||
|
|
||||||
|
fun render() {
|
||||||
|
"<" + name + ">" + inner + "</" + name + ">"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.unaryPlus() {
|
||||||
|
this@Tag.addText(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun html(block: Tag.()->void) {
|
||||||
|
val root = Tag("html")
|
||||||
|
with(root) { block(this) }
|
||||||
|
root.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
val page = html {
|
||||||
|
h3 {
|
||||||
|
+"Heading 3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("<html><h3>Heading 3</h3></html>", page)
|
||||||
|
assertEquals("plain", +"plain")
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContextReceiverExtensionIsHiddenOutsideContext() = runTest {
|
||||||
|
val ex = assertFailsWith<Throwable> {
|
||||||
|
eval("""
|
||||||
|
class Tag {
|
||||||
|
fun wrap(text: String) = "[" + text + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.mark() = this@Tag.wrap(this)
|
||||||
|
|
||||||
|
"x".mark()
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
assertContains(ex.message ?: "", "no such member: mark on String")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testContextReceiverExtensionIsHiddenInWrongContext() = runTest {
|
||||||
|
val ex = assertFailsWith<Throwable> {
|
||||||
|
eval("""
|
||||||
|
class Tag {
|
||||||
|
fun wrap(text: String) = "[" + text + "]"
|
||||||
|
}
|
||||||
|
class Other
|
||||||
|
|
||||||
|
context(Tag)
|
||||||
|
fun String.mark() = this@Tag.wrap(this)
|
||||||
|
|
||||||
|
fun other(block: Other.()->void) {
|
||||||
|
with(Other()) { block(this) }
|
||||||
|
}
|
||||||
|
|
||||||
|
other {
|
||||||
|
"x".mark()
|
||||||
|
}
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
assertContains(ex.message ?: "", "no such member: mark on String")
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testPlusAssignOverloading() = runTest {
|
fun testPlusAssignOverloading() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user