From d6b2b271d42cc996a8e355477d8b353ededbe6ce Mon Sep 17 00:00:00 2001 From: Aleksei Menshutin Date: Sun, 4 Oct 2026 22:54:13 +0300 Subject: [PATCH] feat(ts): preserve yield-star delegation in EtsIR (#386) --- .../main/kotlin/org/jacodb/ets/dto/Convert.kt | 1 + .../main/kotlin/org/jacodb/ets/dto/Values.kt | 1 + .../main/kotlin/org/jacodb/ets/model/Expr.kt | 3 +- .../org/jacodb/ets/test/EtsTsYieldTest.kt | 90 +++++++++++++++++++ jacodb-ets/ts-frontend/src/dto/values.ts | 1 + .../ts-frontend/src/lowering/exprLowering.ts | 1 + jacodb-ets/ts-frontend/test/closures.spec.ts | 15 ++++ 7 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsYieldTest.kt diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt index 9a633674f..1332ef87b 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt @@ -431,6 +431,7 @@ class EtsMethodBuilder( is YieldExprDto -> EtsYieldExpr( arg = arg.toEtsEntity(), type = type.toEtsType(), + isDelegating = isDelegating, ) is TypeOfExprDto -> EtsTypeOfExpr( diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt index 78cdaae37..184afeb8c 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt @@ -225,6 +225,7 @@ data class AwaitExprDto( @SerialName("YieldExpr") data class YieldExprDto( val arg: ValueDto, + val isDelegating: Boolean = false, ) : ExprDto { override val type: TypeDto get() = arg.type diff --git a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt index 7185e94d9..70739cfe3 100644 --- a/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt +++ b/jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt @@ -339,9 +339,10 @@ data class EtsAwaitExpr( data class EtsYieldExpr( override val arg: EtsEntity, override val type: EtsType, + val isDelegating: Boolean = false, ) : EtsUnaryExpr { override fun toString(): String { - return "yield $arg" + return if (isDelegating) "yield* $arg" else "yield $arg" } override fun accept(visitor: EtsExpr.Visitor): R { diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsYieldTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsYieldTest.kt new file mode 100644 index 000000000..6cee43b1d --- /dev/null +++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsYieldTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2022 UnitTestBot contributors (utbot.org) + *

+ * 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 org.jacodb.ets.test + +import kotlin.io.path.createTempDirectory +import kotlin.io.path.exists +import kotlin.io.path.readText +import kotlin.io.path.writeText +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jacodb.ets.dto.EtsFileDto +import org.jacodb.ets.dto.dtoModule +import org.jacodb.ets.dto.toEtsFile +import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME +import org.jacodb.ets.utils.EtsIrProvider +import org.jacodb.ets.utils.generateEtsIR +import org.junit.jupiter.api.Test +import org.jacodb.ets.dto.AssignStmtDto +import org.jacodb.ets.dto.YieldExprDto +import org.jacodb.ets.model.EtsAssignStmt +import org.jacodb.ets.model.EtsYieldExpr + +class EtsTsYieldTest { + companion object { + /** Run the PRODUCTION integration path: generateEtsIR + EtsFileDto.loadFromJson. */ + private fun runFrontend(source: String, fileName: String = "test.ts"): EtsFileDto { + val dir = createTempDirectory("ts-frontend-test") + val inputPath = dir.resolve(fileName) + inputPath.writeText(source) + + val outputPath = generateEtsIR( + inputPath, + isProject = false, + timeout = null, + provider = EtsIrProvider.TS_FRONTEND, + ) + assertTrue(outputPath.exists(), "ts-frontend did not produce output: $outputPath") + + return EtsFileDto.loadFromJson(outputPath.readText()) + } + } + + @Test + fun `yield delegation survives frontend JSON and model conversion`() { + val frontendDto = runFrontend( + source = """ + export function* values() { + yield* [1, 2]; + yield [3]; + } + """.trimIndent(), + ) + val roundTripped = EtsFileDto.loadFromJson( + Json { serializersModule = dtoModule }.encodeToString(frontendDto), + ) + val methodDto = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME } + .methods.single { it.signature.name == "values" } + val yieldsDto = methodDto.body!!.cfg.blocks.flatMap { it.stmts } + .filterIsInstance() + .mapNotNull { it.right as? YieldExprDto } + + assertEquals(listOf(true, false), yieldsDto.map { it.isDelegating }) + + val modelMethod = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME } + .methods.single { it.name == "values" } + val modelYields = modelMethod.cfg.stmts.filterIsInstance() + .mapNotNull { it.rhv as? EtsYieldExpr } + + assertEquals(listOf(true, false), modelYields.map { it.isDelegating }) + assertTrue(modelYields[0].toString().startsWith("yield* ")) + assertTrue(modelYields[1].toString().startsWith("yield ")) + } + +} diff --git a/jacodb-ets/ts-frontend/src/dto/values.ts b/jacodb-ets/ts-frontend/src/dto/values.ts index 9ebac7cea..833f9cae4 100644 --- a/jacodb-ets/ts-frontend/src/dto/values.ts +++ b/jacodb-ets/ts-frontend/src/dto/values.ts @@ -149,6 +149,7 @@ export interface AwaitExprDto { export interface YieldExprDto { readonly _: "YieldExpr"; arg: ValueDto; + isDelegating?: boolean; // Kotlin default: false } export interface TypeOfExprDto { diff --git a/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts b/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts index 2e3b7b17f..998b946a1 100644 --- a/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts +++ b/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts @@ -313,6 +313,7 @@ export class ExprLowerer { arg: node.expression !== undefined ? this.lowerToImmediate(node.expression) : constant("undefined", UNDEFINED_TYPE), + isDelegating: node.asteriskToken !== undefined, }; } throw new LoweringError(ts.SyntaxKind[node.kind]); diff --git a/jacodb-ets/ts-frontend/test/closures.spec.ts b/jacodb-ets/ts-frontend/test/closures.spec.ts index 5c147c1d4..a19a7095d 100644 --- a/jacodb-ets/ts-frontend/test/closures.spec.ts +++ b/jacodb-ets/ts-frontend/test/closures.spec.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { serializeEtsFile } from "../src/serialize"; import { AssignStmtDto, StmtDto } from "../src/dto/stmts"; import { validateEtsFile } from "../src/validate"; import { defaultMethod, lower, methodByName, singleBlockStmts } from "./util"; @@ -417,4 +418,18 @@ describe("generators", () => { const yields = stmts.filter((s) => s._ === "AssignStmt" && s.right._ === "YieldExpr"); expect(yields).toHaveLength(2); }); + + it("preserves yield-star delegation through JSON", () => { + const source = "function* values() { yield* [1, 2]; yield [3]; }"; + const { file, diagnostics } = lower(source); + const roundTripped = JSON.parse(serializeEtsFile(file)); + const statements = methodByName(roundTripped, "values").body!.cfg.blocks.flatMap((block) => block.stmts); + const yields = statements.filter((statement) => statement._ === "AssignStmt" && statement.right._ === "YieldExpr"); + + expect(diagnostics.messages).toEqual([]); + expect(yields.map((statement) => statement.right.isDelegating)).toEqual([true, false]); + + const concrete = new Function(`${source}; return [...values()];`)(); + expect(concrete).toEqual([1, 2, [3]]); + }); });