diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsCallLoweringTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsCallLoweringTest.kt
new file mode 100644
index 000000000..b53f43daa
--- /dev/null
+++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsCallLoweringTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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 kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import org.jacodb.ets.dto.ArrayRefDto
+import org.jacodb.ets.dto.AssignStmtDto
+import org.jacodb.ets.dto.CallExprDto
+import org.jacodb.ets.dto.ClassValueRefDto
+import org.jacodb.ets.dto.EtsFileDto
+import org.jacodb.ets.dto.LocalDto
+import org.jacodb.ets.dto.NewClassExprDto
+import org.jacodb.ets.dto.NewExprDto
+import org.jacodb.ets.dto.PtrCallExprDto
+import org.jacodb.ets.dto.RawValueDto
+import org.jacodb.ets.dto.SpreadExpansionExprDto
+import org.jacodb.ets.dto.TemplateObjectExprDto
+import org.jacodb.ets.dto.dtoModule
+import org.jacodb.ets.dto.toEtsFile
+import org.jacodb.ets.model.EtsAssignStmt
+import org.jacodb.ets.model.EtsNewClassExpr
+import org.jacodb.ets.model.EtsNewExpr
+import org.jacodb.ets.model.EtsPtrCallExpr
+import org.jacodb.ets.model.EtsSpreadExpansionExpr
+import org.jacodb.ets.model.EtsTemplateObjectExpr
+import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME
+import org.jacodb.ets.utils.EtsIrProvider
+import org.jacodb.ets.utils.generateEtsIR
+import org.jacodb.ets.utils.getOperands
+import org.junit.jupiter.api.Test
+import kotlin.io.path.createTempDirectory
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class EtsTsCallLoweringTest {
+ private fun frontendRoundTrip(source: String): EtsFileDto {
+ val input = createTempDirectory("ts-calls-test").resolve("test.ts")
+ input.writeText(source)
+
+ val output = generateEtsIR(
+ input,
+ isProject = false,
+ timeout = null,
+ provider = EtsIrProvider.TS_FRONTEND,
+ )
+ val dto = EtsFileDto.loadFromJson(output.readText())
+ return EtsFileDto.loadFromJson(Json { serializersModule = dtoModule }.encodeToString(dto))
+ }
+
+ private fun assignments(dto: EtsFileDto, method: String): List =
+ dto.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == method }.body!!.cfg.blocks
+ .flatMap { it.stmts }.filterIsInstance()
+
+ @Test
+ fun `comma constructor values decode as locals after computed access`() {
+ val dto = frontendRoundTrip(
+ """
+ class A { x = 1; }
+ export function computed(holder: any): A { return new (holder['sideEffect'], A)(); }
+ export function direct(): A { return new (0, A)(); }
+ """.trimIndent(),
+ )
+
+ for (name in listOf("computed", "direct")) {
+ val stmts = assignments(dto, name)
+ val allocation = stmts.single { it.right is NewExprDto }
+ val constructor = (allocation.right as NewExprDto).constructorValue as LocalDto
+ val capture = stmts.single { it.left == constructor && it.right is ClassValueRefDto }
+
+ assertEquals("A", (capture.right as ClassValueRefDto).signature.name)
+ assertTrue(stmts.indexOf(capture) < stmts.indexOf(allocation))
+ assertTrue(stmts.none { it.right is RawValueDto })
+
+ val model = dto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == name }.cfg.stmts.filterIsInstance()
+ val newExpr = model.single { it.rhv is EtsNewExpr }.rhv as EtsNewExpr
+ assertTrue(newExpr.constructorValue != null)
+ }
+ }
+
+ @Test
+ fun `call and constructor spreads preserve iterator expansion and ordered arguments`() {
+ val dto = frontendRoundTrip(
+ """
+ function add(a: number, b: number): number { return a + b; }
+ class Point { constructor(public x: number, public y: number) {} }
+ export function sum(pair: [number, number]): number { return add(...pair); }
+ export function make(pair: [number, number]): number { return new Point(...pair).y; }
+ """.trimIndent(),
+ )
+
+ for ((name, target) in listOf("sum" to "add", "make" to "constructor")) {
+ val stmts = assignments(dto, name)
+ val expansion = stmts.single { it.right is SpreadExpansionExprDto }
+ val reads = stmts.filter { it.right is ArrayRefDto }
+ val call = stmts.single { (it.right as? CallExprDto)?.method?.name == target }.right as CallExprDto
+
+ assertEquals(2, (expansion.right as SpreadExpansionExprDto).expectedCount)
+ assertEquals(2, reads.size)
+ assertEquals(reads.map { it.left }, call.args)
+ assertTrue(reads.all { (it.right as ArrayRefDto).array == expansion.left })
+ assertTrue(stmts.indexOf(expansion) < stmts.indexOf(reads.first()))
+ assertTrue(stmts.none { it.right is RawValueDto })
+
+ val model = dto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == name }.cfg.stmts.filterIsInstance()
+ val expanded = model.single { it.rhv is EtsSpreadExpansionExpr }.rhv as EtsSpreadExpansionExpr
+ assertEquals(2, expanded.expectedCount)
+ assertEquals(listOf(expanded.iterable), expanded.getOperands().toList())
+ }
+ }
+
+ @Test
+ fun `class expressions retain constructor identity field initialization and visitor support`() {
+ val dto = frontendRoundTrip(
+ """
+ export function value(): number {
+ const Local = class { x = 1; };
+ return new Local().x;
+ }
+ """.trimIndent(),
+ )
+ val stmts = assignments(dto, "value")
+ val creation = stmts.single { it.right is NewClassExprDto }.right as NewClassExprDto
+ val clazz = dto.classes.single { it.signature == creation.signature }
+ val allocation = stmts.single { it.right is NewExprDto }.right as NewExprDto
+
+ assertEquals(creation.signature, (allocation.classType as org.jacodb.ets.dto.ClassTypeDto).signature)
+ assertTrue(clazz.fields.any { it.signature.name == "x" })
+ assertTrue(clazz.methods.any { it.signature.name == "%instInit" && it.body != null })
+ assertTrue(clazz.methods.any { it.signature.name == "constructor" && it.body != null })
+ assertTrue(stmts.none { it.right is RawValueDto })
+
+ val model = dto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "value" }.cfg.stmts.filterIsInstance()
+ val newClass = model.single { it.rhv is EtsNewClassExpr }.rhv as EtsNewClassExpr
+ assertTrue(newClass.getOperands().none())
+ assertEquals(creation.signature.name, newClass.signature.name)
+ }
+
+ @Test
+ fun `tagged template objects preserve raw cooked undefined sites and method receivers`() {
+ val dto = frontendRoundTrip(
+ """
+ class Tagger {
+ tag(parts: TemplateStringsArray, value: number): string { return parts.raw[0] + value; }
+ }
+ export function value(tagger: Tagger): string { return tagger.tag`\unicode${'$'}{1}`; }
+ """.trimIndent(),
+ )
+ val stmts = assignments(dto, "value")
+ val creation = stmts.single { it.right is TemplateObjectExprDto }
+ val template = creation.right as TemplateObjectExprDto
+ val call = stmts.single { it.right is PtrCallExprDto }.right as PtrCallExprDto
+
+ assertEquals(listOf(null, ""), template.cooked)
+ assertEquals(listOf("\\unicode", ""), template.raw)
+ assertTrue(template.siteId.isNotEmpty())
+ assertEquals(creation.left, call.args.first())
+ assertTrue(call.receiver != null)
+ assertTrue(stmts.none { it.right is RawValueDto })
+
+ val model = dto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "value" }.cfg.stmts.filterIsInstance()
+ val modelTemplate = model.single { it.rhv is EtsTemplateObjectExpr }.rhv as EtsTemplateObjectExpr
+ val modelCall = model.single { it.rhv is EtsPtrCallExpr }.rhv as EtsPtrCallExpr
+ assertEquals(template.siteId, modelTemplate.siteId)
+ assertEquals(template.cooked, modelTemplate.cooked)
+ assertEquals(template.raw, modelTemplate.raw)
+ assertTrue(modelTemplate.getOperands().none())
+ assertTrue(modelCall.getOperands().any { it == modelCall.receiver })
+ }
+}
diff --git a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsFrontendTest.kt b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsFrontendTest.kt
index 1c7655731..be2b23284 100644
--- a/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsFrontendTest.kt
+++ b/jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsFrontendTest.kt
@@ -16,71 +16,93 @@
package org.jacodb.ets.test
+import java.math.BigInteger
+import kotlin.io.path.createDirectories
+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.assertFalse
+import kotlin.test.assertTrue
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jacodb.ets.dto.ArrayRefDto
import org.jacodb.ets.dto.ArrayTypeDto
import org.jacodb.ets.dto.AssignStmtDto
-import org.jacodb.ets.dto.LexicalEnvTypeDto
-import org.jacodb.ets.dto.FunctionTypeDto
-import org.jacodb.ets.dto.DefineAccessorStmtDto
-import org.jacodb.ets.dto.CopyDataPropertiesStmtDto
+import org.jacodb.ets.dto.AwaitExprDto
+import org.jacodb.ets.dto.BigIntTypeDto
import org.jacodb.ets.dto.BooleanTypeDto
import org.jacodb.ets.dto.CaughtExceptionRefDto
import org.jacodb.ets.dto.ClassTypeDto
import org.jacodb.ets.dto.ClassValueRefDto
import org.jacodb.ets.dto.ClassValueTypeDto
import org.jacodb.ets.dto.ConstantDto
+import org.jacodb.ets.dto.CopyDataPropertiesStmtDto
+import org.jacodb.ets.dto.DefineAccessorStmtDto
import org.jacodb.ets.dto.DefineDataPropertyStmtDto
import org.jacodb.ets.dto.EtsFileDto
+import org.jacodb.ets.dto.FunctionTypeDto
import org.jacodb.ets.dto.IfStmtDto
+import org.jacodb.ets.dto.InstanceCallExprDto
import org.jacodb.ets.dto.InstanceOfExprDto
+import org.jacodb.ets.dto.LexicalEnvTypeDto
import org.jacodb.ets.dto.LocalDto
import org.jacodb.ets.dto.NewArrayExprDto
import org.jacodb.ets.dto.NewExprDto
import org.jacodb.ets.dto.NumberTypeDto
import org.jacodb.ets.dto.Ops
import org.jacodb.ets.dto.PropertyRefDto
+import org.jacodb.ets.dto.PtrCallExprDto
import org.jacodb.ets.dto.RawStmtDto
import org.jacodb.ets.dto.RawValueDto
import org.jacodb.ets.dto.RelationOperationDto
-import org.jacodb.ets.dto.ReturnStmtDto
import org.jacodb.ets.dto.RequireObjectCoercibleExprDto
-import org.jacodb.ets.dto.StringTypeDto
+import org.jacodb.ets.dto.ReturnStmtDto
import org.jacodb.ets.dto.StaticCallExprDto
import org.jacodb.ets.dto.StaticFieldRefDto
+import org.jacodb.ets.dto.StringTypeDto
+import org.jacodb.ets.dto.SymbolTypeDto
import org.jacodb.ets.dto.ThrowStmtDto
import org.jacodb.ets.dto.ToPropertyKeyExprDto
import org.jacodb.ets.dto.UnaryOperationDto
import org.jacodb.ets.dto.UnknownTypeDto
import org.jacodb.ets.dto.ValueDto
+import org.jacodb.ets.dto.YieldExprDto
import org.jacodb.ets.dto.dtoModule
import org.jacodb.ets.dto.toEtsFile
import org.jacodb.ets.model.EtsArrayAccess
import org.jacodb.ets.model.EtsAssignStmt
-import org.jacodb.ets.model.EtsStringConstant
-import org.jacodb.ets.model.EtsDefineAccessorStmt
-import org.jacodb.ets.model.EtsCopyDataPropertiesStmt
-import org.jacodb.ets.model.EtsCaughtExceptionRef
+import org.jacodb.ets.model.EtsAwaitExpr
+import org.jacodb.ets.model.EtsBigIntConstant
+import org.jacodb.ets.model.EtsBigIntType
import org.jacodb.ets.model.EtsCallStmt
+import org.jacodb.ets.model.EtsCaughtExceptionRef
import org.jacodb.ets.model.EtsClassValueRef
import org.jacodb.ets.model.EtsClassValueType
import org.jacodb.ets.model.EtsClosureFieldRef
+import org.jacodb.ets.model.EtsCopyDataPropertiesStmt
+import org.jacodb.ets.model.EtsDefineAccessorStmt
import org.jacodb.ets.model.EtsDefineDataPropertyStmt
import org.jacodb.ets.model.EtsEqExpr
+import org.jacodb.ets.model.EtsFunctionType
import org.jacodb.ets.model.EtsIfStmt
import org.jacodb.ets.model.EtsInstanceFieldRef
import org.jacodb.ets.model.EtsInstanceOfExpr
import org.jacodb.ets.model.EtsLocal
+import org.jacodb.ets.model.EtsNewArrayExpr
import org.jacodb.ets.model.EtsNewExpr
+import org.jacodb.ets.model.EtsNumberConstant
import org.jacodb.ets.model.EtsPropertyRef
+import org.jacodb.ets.model.EtsPtrCallExpr
import org.jacodb.ets.model.EtsRawEntity
import org.jacodb.ets.model.EtsRawStmt
import org.jacodb.ets.model.EtsScene
-import org.jacodb.ets.model.EtsNewArrayExpr
-import org.jacodb.ets.model.EtsNumberConstant
+import org.jacodb.ets.model.EtsStringConstant
+import org.jacodb.ets.model.EtsSymbolType
import org.jacodb.ets.model.EtsThrowStmt
import org.jacodb.ets.model.EtsToPropertyKeyExpr
+import org.jacodb.ets.model.EtsYieldExpr
import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME
import org.jacodb.ets.utils.DEFAULT_ARK_METHOD_NAME
import org.jacodb.ets.utils.EtsIrProvider
@@ -88,14 +110,6 @@ import org.jacodb.ets.utils.defaultProviderFor
import org.jacodb.ets.utils.generateEtsIR
import org.jacodb.ets.utils.getOperands
import org.junit.jupiter.api.Test
-import kotlin.io.path.createDirectories
-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.assertFalse
-import kotlin.test.assertTrue
/**
* Tests for the native TypeScript frontend (`jacodb-ets/ts-frontend`).
@@ -126,43 +140,235 @@ class EtsTsFrontendTest {
}
@Test
- fun `computed object binding reuses its normalized key in rest through JSON conversion`() {
+ fun `symbol parameter and return types survive frontend JSON and model conversion`() {
+ val frontendDto = runFrontend("export function same(value: symbol): symbol { return value; }")
+ 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 == "same" }
+
+ assertEquals(SymbolTypeDto, methodDto.signature.parameters.single().type)
+ assertEquals(SymbolTypeDto, methodDto.signature.returnType)
+
+ val modelMethod = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "same" }
+
+ assertEquals(EtsSymbolType, modelMethod.signature.parameters.single().type)
+ assertEquals(EtsSymbolType, modelMethod.signature.returnType)
+ }
+
+ @Test
+ fun `stored arrow lexical receiver survives JSON and model conversion`() {
val frontendDto = runFrontend(
"""
- export function pick(input: any, key: any): number {
- const { [key]: value, ...rest } = input;
- return value + rest.y;
+ class Box {
+ offset = 11;
+ installOn(target: Box) {
+ target.callback = (value: number) => this.offset + value;
+ }
+ installOrdinaryOn(target: Box) {
+ target.callback = function(value: number) { return this.offset + value; };
+ }
}
""".trimIndent(),
)
+ val roundTripped = EtsFileDto.loadFromJson(
+ Json { serializersModule = dtoModule }.encodeToString(frontendDto),
+ )
+ val box = roundTripped.toEtsFile().classes.single { it.name == "Box" }
+ val install = box.methods.single { it.name == "installOn" }
+ val arrowValue = install.cfg.stmts.filterIsInstance()
+ .map { it.rhv }
+ .filterIsInstance()
+ .single { it.name.startsWith("%AM") }
+ val arrow = box.methods.single { it.name == arrowValue.name }
+ val ordinary = box.methods.single { it.name.endsWith("\$installOrdinaryOn") }
+
+ assertTrue((arrowValue.type as EtsFunctionType).isArrow)
+ val receiver = arrow.cfg.stmts.filterIsInstance()
+ .single { (it.lhv as? EtsLocal)?.name == "this" }.rhv
+ assertTrue(receiver is EtsClosureFieldRef)
+ assertEquals("this", receiver.fieldName)
+ assertTrue(ordinary.cfg.stmts.filterIsInstance()
+ .single { (it.lhv as? EtsLocal)?.name == "this" }.rhv is org.jacodb.ets.model.EtsThis)
+ }
+
+ @Test
+ fun `bigint types and exact constants survive frontend JSON and model conversion`() {
+ val frontendDto = runFrontend(
+ """
+ export function increment(x: bigint): bigint { return x + 1n; }
+ export const large = 9007199254740993n;
+ export const negative = -0x20n;
+ """.trimIndent(),
+ )
+ val roundTripped = EtsFileDto.loadFromJson(
+ Json { serializersModule = dtoModule }.encodeToString(frontendDto),
+ )
+ val defaultClass = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ val incrementDto = defaultClass.methods.single { it.signature.name == "increment" }
+ val constantsDto = defaultClass.methods.flatMap { it.body?.cfg?.blocks.orEmpty() }
+ .flatMap { it.stmts }
+ .filterIsInstance()
+ .mapNotNull { it.right as? ConstantDto }
+ .filter { it.type == BigIntTypeDto }
+
+ assertEquals(BigIntTypeDto, incrementDto.signature.parameters.single().type)
+ assertEquals(BigIntTypeDto, incrementDto.signature.returnType)
+ assertTrue(constantsDto.any { it.value == "9007199254740993" })
+ assertTrue(constantsDto.any { it.value == "-32" })
+
+ val modelClass = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ val incrementModel = modelClass.methods.single { it.name == "increment" }
+ val bigInts = modelClass.methods.flatMap { it.cfg.stmts }
+ .filterIsInstance()
+ .mapNotNull { it.rhv as? EtsBigIntConstant }
+
+ assertEquals(EtsBigIntType, incrementModel.signature.parameters.single().type)
+ assertEquals(EtsBigIntType, incrementModel.signature.returnType)
+ assertTrue(bigInts.any { it.value == BigInteger("9007199254740993") })
+ assertTrue(bigInts.any { it.value == BigInteger("-32") })
+ }
+
+ @Test
+ fun `for await preserves async acquisition and sync value unwrapping`() {
+ val frontendDto = runFrontend(
+ """
+ export async function first(input: AsyncIterable): Promise {
+ for await (const value of input) return value;
+ return -1;
+ }
+ export async function fromArray(input: number[]): Promise {
+ for await (const value of input) return value;
+ return -1;
+ }
+ """.trimIndent(),
+ )
+ val roundTripped = EtsFileDto.loadFromJson(
+ Json { serializersModule = dtoModule }.encodeToString(frontendDto),
+ )
+ for (name in listOf("first", "fromArray")) {
+ val methodDto = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == name }
+ val assignments = methodDto.body!!.cfg.blocks.flatMap { it.stmts }.filterIsInstance()
+ val calls = assignments.mapNotNull { it.right as? PtrCallExprDto }
+ val awaitAssignments = assignments.filter { it.right is AwaitExprDto }
+ val next = assignments.single { (it.right as? PtrCallExprDto)?.method?.name == "next" }
+ val iterator = (next.right as PtrCallExprDto).receiver
+ val nextAwait = awaitAssignments.single { (it.right as AwaitExprDto).arg == next.left }
+ val nextValueReads = assignments.filter {
+ (it.right as? org.jacodb.ets.dto.InstanceFieldRefDto)?.let { field ->
+ field.field.name == "value" && field.instance == nextAwait.left
+ } == true
+ }
+ val syncValueAwait = awaitAssignments.single {
+ (it.right as AwaitExprDto).arg == nextValueReads.first().left
+ }
+ val returnLookup = assignments.single {
+ (it.right as? org.jacodb.ets.dto.InstanceFieldRefDto)?.let { field ->
+ field.field.name == "return" && field.instance == iterator
+ } == true
+ }
+ val close = assignments.single { (it.right as? PtrCallExprDto)?.method?.name == "return" }
+ val closeCall = close.right as PtrCallExprDto
+ val closeValueRead = assignments.single {
+ (it.right as? org.jacodb.ets.dto.InstanceFieldRefDto)?.let { field ->
+ field.field.name == "value" && field.instance == close.left
+ } == true
+ }
+ val asyncCloseAwait = awaitAssignments.single { (it.right as AwaitExprDto).arg == close.left }
+ val syncCloseAwait = awaitAssignments.single { (it.right as AwaitExprDto).arg == closeValueRead.left }
+ val missingReturnAwait = awaitAssignments.single {
+ ((it.right as AwaitExprDto).arg as? org.jacodb.ets.dto.ConstantDto)?.value == "undefined"
+ }
+
+ assertTrue(calls.any { it.method.name == "Symbol.asyncIterator" && it.receiver != null })
+ assertTrue(calls.any { it.method.name == "Symbol.iterator" && it.receiver != null })
+ assertTrue(iterator != null)
+ assertEquals(2, nextValueReads.size)
+ assertTrue(nextValueReads.all { it.left == syncValueAwait.left })
+ assertEquals(returnLookup.left, closeCall.ptr)
+ assertEquals(iterator, closeCall.receiver)
+ assertTrue(assignments.any {
+ (it.right as? org.jacodb.ets.dto.InstanceFieldRefDto)?.let { field ->
+ field.field.name == "done" && field.instance == close.left
+ } == true
+ })
+ assertEquals(
+ setOf(nextAwait, syncValueAwait, asyncCloseAwait, syncCloseAwait, missingReturnAwait),
+ awaitAssignments.toSet(),
+ )
+ assertEquals(5, awaitAssignments.size)
+ assertTrue(assignments.none { it.right is RawValueDto })
+
+ val modelMethod = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == name }
+ val modelAssignments = modelMethod.cfg.stmts.filterIsInstance()
+ val modelAwaits = modelAssignments.mapNotNull { it.rhv as? EtsAwaitExpr }
+ val modelNext = modelAssignments.single {
+ (it.rhv as? org.jacodb.ets.model.EtsPtrCallExpr)?.callee?.name == "next"
+ }
+ val modelClose = modelAssignments.single {
+ (it.rhv as? org.jacodb.ets.model.EtsPtrCallExpr)?.callee?.name == "return"
+ }
+
+ assertEquals(awaitAssignments.size, modelAwaits.size)
+ assertEquals(
+ (iterator as org.jacodb.ets.dto.LocalDto).name,
+ (modelNext.rhv as org.jacodb.ets.model.EtsPtrCallExpr).receiver?.name,
+ )
+ assertEquals(
+ (modelNext.rhv as org.jacodb.ets.model.EtsPtrCallExpr).receiver,
+ (modelClose.rhv as org.jacodb.ets.model.EtsPtrCallExpr).receiver,
+ )
+ for (dtoAwait in listOf(nextAwait, syncValueAwait, asyncCloseAwait, syncCloseAwait)) {
+ val argument = (dtoAwait.right as AwaitExprDto).arg as org.jacodb.ets.dto.LocalDto
+ assertTrue(modelAwaits.any { it.arg.name == argument.name })
+ }
+ }
+ }
+
+ @Test
+ fun `computed object keys survive the frontend JSON round trip`() {
+ val frontendDto = runFrontend(
+ """
+ export function read(key: string): number {
+ const object = { [key]: 1 };
+ const { [key]: value } = object;
+ return value;
+ }
+ """.trimIndent(),
+ )
+
val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
val roundTripped = EtsFileDto.loadFromJson(serialized)
val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.signature.name == "pick" }.body!!.cfg.blocks.flatMap { it.stmts }
+ .methods.single { it.signature.name == "read" }
+ .body!!.cfg.blocks.flatMap { it.stmts }
val assignments = dtoStmts.filterIsInstance()
- val read = assignments.mapNotNull { it.right as? PropertyRefDto }.single()
- val copy = dtoStmts.filterIsInstance().single()
- assertEquals(expected = 1, actual = assignments.count { it.right is ToPropertyKeyExprDto })
- assertEquals(expected = read.key, actual = copy.excludedKeys.single())
+ assertEquals(1, dtoStmts.filterIsInstance().size)
+ assertTrue(assignments.any { it.right is PropertyRefDto })
+ assertEquals(2, assignments.count { it.right is ToPropertyKeyExprDto })
assertTrue(assignments.none { it.right is RawValueDto })
- val method = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.name == "pick" }
+ val scene = EtsScene(listOf(roundTripped.toEtsFile()))
+ val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "read" }
val converted = method.cfg.stmts.filterIsInstance()
- val modelRead = converted.mapNotNull { it.rhv as? EtsPropertyRef }.single()
- val modelCopy = method.cfg.stmts.filterIsInstance().single()
-
- assertEquals(expected = 1, actual = converted.count { it.rhv is EtsToPropertyKeyExpr })
- assertEquals(expected = modelRead.key, actual = modelCopy.excludedKeys.single())
+ assertEquals(1, method.cfg.stmts.filterIsInstance().size)
+ assertTrue(converted.any { it.rhv is EtsPropertyRef })
+ assertEquals(2, converted.count { it.rhv is EtsToPropertyKeyExpr })
}
@Test
- fun `object rest preserves excluded keys through JSON conversion`() {
+ fun `object spread and rest preserve copy operations through JSON conversion`() {
val frontendDto = runFrontend(
"""
export function copy(input: { x: number; y: number }): number {
- const { x, ...rest } = input;
+ const object = { ...input, y: 7 };
+ const { x, ...rest } = object;
return rest.y + x;
}
""".trimIndent(),
@@ -175,17 +381,17 @@ class EtsTsFrontendTest {
.body!!.cfg.blocks.flatMap { it.stmts }
val copies = dtoStmts.filterIsInstance()
- assertTrue(dtoStmts.filterIsInstance().any { it.right is RequireObjectCoercibleExprDto })
- assertEquals(1, copies.size)
- assertTrue(copies[0].throwOnNullishSource)
- assertEquals("x", (copies[0].excludedKeys.single() as ConstantDto).value)
+ assertEquals(2, copies.size)
+ assertFalse(copies[0].throwOnNullishSource)
+ assertTrue(copies[1].throwOnNullishSource)
+ assertEquals("x", (copies[1].excludedKeys.single() as ConstantDto).value)
val scene = EtsScene(listOf(roundTripped.toEtsFile()))
val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
.methods.single { it.name == "copy" }
val converted = method.cfg.stmts.filterIsInstance()
- assertEquals(1, converted.size)
- assertEquals("x", (converted[0].excludedKeys.single() as EtsStringConstant).value)
+ assertEquals(2, converted.size)
+ assertEquals("x", (converted[1].excludedKeys.single() as EtsStringConstant).value)
}
@Test
@@ -219,65 +425,59 @@ class EtsTsFrontendTest {
}
@Test
- fun `object spread preserves ordered copy operations through JSON conversion`() {
- val frontendDto = runFrontend(
+ fun `array expansion and parameter defaults survive frontend JSON conversion`() {
+ val dto = runFrontend(
"""
- export function copy(input: { x: number; y: number }): number {
- const object = { ...input, y: 7 };
- return object.y + object.x;
- }
+ export function defaults(x = 3): number { return x; }
+ export function spread(input: number[]): number[] { return [0, ...input]; }
+ export function tail(input: number[]): number[] { const [first, ...rest] = input; return rest; }
+ export function count(...values: number[]): number { return values.length; }
""".trimIndent(),
)
-
- val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
+ val serialized = Json { serializersModule = dtoModule }.encodeToString(dto)
val roundTripped = EtsFileDto.loadFromJson(serialized)
- val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.signature.name == "copy" }
- .body!!.cfg.blocks.flatMap { it.stmts }
- val copies = dtoStmts.filterIsInstance()
-
- assertEquals(1, copies.size)
- assertFalse(copies[0].throwOnNullishSource)
- assertTrue(copies[0].excludedKeys.isEmpty())
+ val defaultClass = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ val modelClass = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
- val scene = EtsScene(listOf(roundTripped.toEtsFile()))
- val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.name == "copy" }
- val converted = method.cfg.stmts.filterIsInstance()
- assertEquals(1, converted.size)
- assertTrue(converted[0].excludedKeys.isEmpty())
+ assertTrue(defaultClass.methods.single { it.signature.name == "count" }.signature.parameters.single().isRest)
+ assertTrue(modelClass.methods.single { it.name == "defaults" }.cfg.stmts.any { it is EtsIfStmt })
+ assertTrue(modelClass.methods.single { it.name == "spread" }.cfg.stmts.any {
+ it is EtsAssignStmt && it.rhv is EtsNewArrayExpr
+ })
+ assertTrue(modelClass.methods.single { it.name == "tail" }.cfg.stmts.any {
+ it is EtsAssignStmt && it.rhv is EtsNewArrayExpr
+ })
+ val spreadCalls = defaultClass.methods.single { it.signature.name == "spread" }.body!!.cfg.blocks
+ .flatMap { it.stmts }.filterIsInstance().mapNotNull { it.right as? PtrCallExprDto }
+ val modelCalls = modelClass.methods.single { it.name == "spread" }.cfg.stmts
+ .filterIsInstance().mapNotNull { it.rhv as? EtsPtrCallExpr }
+
+ assertEquals(1, spreadCalls.size)
+ assertEquals(1, modelCalls.size)
+ assertTrue(spreadCalls.single().receiver is LocalDto)
+ assertEquals((spreadCalls.single().receiver as LocalDto).name, modelCalls.single().receiver!!.name)
+ assertTrue(modelCalls.single().getOperands().any { it == modelCalls.single().receiver })
}
@Test
- fun `computed object keys survive the frontend JSON round trip`() {
- val frontendDto = runFrontend(
- """
- export function read(key: string): number {
- const object = { [key]: 1 };
- return object[key];
- }
- """.trimIndent(),
- )
-
- val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
+ fun `regular expression literal preserves pattern and flags through JSON conversion`() {
+ val dto = runFrontend("export function containsX(value: string): boolean { return /x+/gi.test(value); }")
+ val serialized = Json { serializersModule = dtoModule }.encodeToString(dto)
val roundTripped = EtsFileDto.loadFromJson(serialized)
- val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.signature.name == "read" }
- .body!!.cfg.blocks.flatMap { it.stmts }
- val assignments = dtoStmts.filterIsInstance()
-
- assertEquals(1, dtoStmts.filterIsInstance().size)
- assertTrue(assignments.any { it.right is PropertyRefDto })
- assertEquals(2, assignments.count { it.right is ToPropertyKeyExprDto })
- assertTrue(assignments.none { it.right is RawValueDto })
-
- val scene = EtsScene(listOf(roundTripped.toEtsFile()))
- val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
- .methods.single { it.name == "read" }
- val converted = method.cfg.stmts.filterIsInstance()
- assertEquals(1, method.cfg.stmts.filterIsInstance().size)
- assertTrue(converted.any { it.rhv is EtsPropertyRef })
- assertEquals(2, converted.count { it.rhv is EtsToPropertyKeyExpr })
+ val method = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == "containsX" }
+ val assignments = method.body!!.cfg.blocks.flatMap { it.stmts }.filterIsInstance()
+ val constructor = assignments.mapNotNull { it.right as? InstanceCallExprDto }
+ .single { it.method.name == "constructor" }
+ val testCall = assignments.mapNotNull { it.right as? PtrCallExprDto }
+ .single { it.method.name == "test" }
+
+ assertEquals(listOf("x+", "gi"), constructor.args.map { (it as ConstantDto).value })
+ assertTrue(testCall.receiver is LocalDto)
+ assertTrue(roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "containsX" }.cfg.stmts.any {
+ it is EtsAssignStmt && it.rhv is EtsNewExpr
+ })
}
@Test
@@ -503,7 +703,7 @@ class EtsTsFrontendTest {
}
@Test
- fun `computed constructor access stays unsupported through JSON and model conversion`() {
+ fun `computed constructor access keeps property semantics through JSON and model conversion`() {
val frontendDto = runFrontend(
"""
class A {}
@@ -516,15 +716,18 @@ class EtsTsFrontendTest {
.methods.single { it.signature.name == "make" }
val assignments = methodDto.body!!.cfg.blocks.flatMap { it.stmts }.filterIsInstance()
- assertTrue(assignments.any { (it.right as? RawValueDto)?.kind == "UnsupportedValue" })
- assertTrue(assignments.none { it.right is ArrayRefDto || it.right is NewExprDto })
+ val property = assignments.single { it.right is org.jacodb.ets.dto.PropertyRefDto }
+ val allocation = assignments.single { it.right is NewExprDto }.right as NewExprDto
+ assertEquals(property.left as LocalDto, allocation.constructorValue as LocalDto)
+ assertTrue(assignments.none { it.right is ArrayRefDto || it.right is RawValueDto })
val modelMethod = frontendDto.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
.methods.single { it.name == "make" }
val modelAssignments = modelMethod.cfg.stmts.filterIsInstance()
- assertTrue(modelAssignments.any { it.rhv is EtsRawEntity })
- assertTrue(modelAssignments.none { it.rhv is EtsNewExpr })
+ assertTrue(modelAssignments.any { it.rhv is org.jacodb.ets.model.EtsPropertyRef })
+ assertTrue(modelAssignments.any { it.rhv is EtsNewExpr })
+ assertTrue(modelAssignments.none { it.rhv is EtsRawEntity })
}
@Test
@@ -1093,6 +1296,37 @@ class EtsTsFrontendTest {
assertTrue(method.cfg.throwers(catcher).containsAll(listOf(comparison, fieldStore)))
}
+ @Test
+ fun `yield delegation survives frontend JSON and model conversion`() {
+ val frontendDto = runFrontend(
+ """
+ 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 "))
+ }
+
@Test
fun `closures, destructuring and object literals convert to the model`() {
val etsFileDto = runFrontend(
@@ -1174,4 +1408,97 @@ class EtsTsFrontendTest {
val method = clazz.methods.single { it.name == DEFAULT_ARK_METHOD_NAME }
assertTrue(method.cfg.stmts.isNotEmpty(), "default method must have a non-empty body")
}
+
+ @Test
+ fun `computed object binding reuses its normalized key in rest through JSON conversion`() {
+ val frontendDto = runFrontend(
+ """
+ export function pick(input: any, key: any): number {
+ const { [key]: value, ...rest } = input;
+ return value + rest.y;
+ }
+ """.trimIndent(),
+ )
+ val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
+ val roundTripped = EtsFileDto.loadFromJson(serialized)
+ val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == "pick" }.body!!.cfg.blocks.flatMap { it.stmts }
+ val assignments = dtoStmts.filterIsInstance()
+ val read = assignments.mapNotNull { it.right as? PropertyRefDto }.single()
+ val copy = dtoStmts.filterIsInstance().single()
+
+ assertEquals(expected = 1, actual = assignments.count { it.right is ToPropertyKeyExprDto })
+ assertEquals(expected = read.key, actual = copy.excludedKeys.single())
+ assertTrue(assignments.none { it.right is RawValueDto })
+
+ val method = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "pick" }
+ val converted = method.cfg.stmts.filterIsInstance()
+ val modelRead = converted.mapNotNull { it.rhv as? EtsPropertyRef }.single()
+ val modelCopy = method.cfg.stmts.filterIsInstance().single()
+
+ assertEquals(expected = 1, actual = converted.count { it.rhv is EtsToPropertyKeyExpr })
+ assertEquals(expected = modelRead.key, actual = modelCopy.excludedKeys.single())
+ }
+
+ @Test
+ fun `object rest preserves excluded keys through JSON conversion`() {
+ val frontendDto = runFrontend(
+ """
+ export function copy(input: { x: number; y: number }): number {
+ const { x, ...rest } = input;
+ return rest.y + x;
+ }
+ """.trimIndent(),
+ )
+
+ val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
+ val roundTripped = EtsFileDto.loadFromJson(serialized)
+ val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == "copy" }
+ .body!!.cfg.blocks.flatMap { it.stmts }
+ val copies = dtoStmts.filterIsInstance()
+
+ assertTrue(dtoStmts.filterIsInstance().any { it.right is RequireObjectCoercibleExprDto })
+ assertEquals(1, copies.size)
+ assertTrue(copies[0].throwOnNullishSource)
+ assertEquals("x", (copies[0].excludedKeys.single() as ConstantDto).value)
+
+ val scene = EtsScene(listOf(roundTripped.toEtsFile()))
+ val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "copy" }
+ val converted = method.cfg.stmts.filterIsInstance()
+ assertEquals(1, converted.size)
+ assertEquals("x", (converted[0].excludedKeys.single() as EtsStringConstant).value)
+ }
+
+ @Test
+ fun `object spread preserves ordered copy operations through JSON conversion`() {
+ val frontendDto = runFrontend(
+ """
+ export function copy(input: { x: number; y: number }): number {
+ const object = { ...input, y: 7 };
+ return object.y + object.x;
+ }
+ """.trimIndent(),
+ )
+
+ val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
+ val roundTripped = EtsFileDto.loadFromJson(serialized)
+ val dtoStmts = roundTripped.classes.single { it.signature.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.signature.name == "copy" }
+ .body!!.cfg.blocks.flatMap { it.stmts }
+ val copies = dtoStmts.filterIsInstance()
+
+ assertEquals(1, copies.size)
+ assertFalse(copies[0].throwOnNullishSource)
+ assertTrue(copies[0].excludedKeys.isEmpty())
+
+ val scene = EtsScene(listOf(roundTripped.toEtsFile()))
+ val method = scene.projectClasses.single { it.name == DEFAULT_ARK_CLASS_NAME }
+ .methods.single { it.name == "copy" }
+ val converted = method.cfg.stmts.filterIsInstance()
+ assertEquals(1, converted.size)
+ assertTrue(converted[0].excludedKeys.isEmpty())
+ }
}
diff --git a/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts b/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts
index 4634295ec..2df839ba4 100644
--- a/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts
+++ b/jacodb-ets/ts-frontend/src/lowering/exprLowering.ts
@@ -1486,7 +1486,6 @@ export class ExprLowerer {
: { _: "PtrCallExpr", ptr, receiver, method, args };
}
- /** `a${x}b` -> chain of string `+` binops. */
private lowerArrayLiteralWithSpread(
node: ts.ArrayLiteralExpression,
arrayType: TypeDto,
@@ -1528,6 +1527,7 @@ export class ExprLowerer {
return result;
}
+ /** `a${x}b` -> chain of string `+` binops. */
private lowerTemplate(node: ts.TemplateExpression): ValueDto {
let acc: ImmediateDto = constant(node.head.text, STRING_TYPE);
for (const span of node.templateSpans) {
diff --git a/jacodb-ets/ts-frontend/test/array-and-params-semantics.spec.ts b/jacodb-ets/ts-frontend/test/array-and-params-semantics.spec.ts
new file mode 100644
index 000000000..4a1ae458b
--- /dev/null
+++ b/jacodb-ets/ts-frontend/test/array-and-params-semantics.spec.ts
@@ -0,0 +1,175 @@
+import { describe, expect, it } from "vitest";
+import * as ts from "typescript";
+import { serializeEtsFile } from "../src/serialize";
+import { executor } from "./execute";
+import { lower } from "./util";
+
+function consumers(source: string, name: string, externals: Record = {}) {
+ const { file, diagnostics } = lower(source);
+ expect(diagnostics.messages).toEqual([]);
+ const ir = executor(JSON.parse(serializeEtsFile(file)), externals);
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const native = new Function(...Object.keys(externals), `${js}; return ${name};`)(...Object.values(externals));
+ return { ir: (...args: any[]) => ir.call(name, ...args), native, initialize: () => ir.initialize() };
+}
+
+describe("array iterator behavior after JSON round trip", () => {
+ for (const source of [
+ "function collect(input: any): any[] { return [...input]; }",
+ "function collect(input: any): any[] { const [...rest] = input; return rest; }",
+ ]) {
+ it(`uses the selected iterator and caches next: ${source}`, () => {
+ const { ir, native } = consumers(source, "collect");
+ const input = () => {
+ let nextReads = 0;
+ let count = 0;
+ const next = function (this: any) {
+ expect(this.marker).toBe(42);
+ return { value: ++count, done: count > 2 ? "finished" : 0 };
+ };
+ next.call = () => { throw new Error("A user-defined .call must not be read"); };
+ return {
+ length: 100,
+ [Symbol.iterator]() {
+ return { marker: 42, get next() { expect(++nextReads).toBe(1); return next; } };
+ },
+ };
+ };
+
+ expect(ir(input())).toEqual(native(input()));
+ expect(ir(input())).toEqual([1, 2]);
+ });
+
+ it(`rejects invalid iterator results: ${source}`, () => {
+ const { ir, native } = consumers(source, "collect");
+ const malformed = () => ({ [Symbol.iterator]: () => ({ next: () => 1 }) });
+
+ expect(() => native(malformed())).toThrow(TypeError);
+ expect(() => ir(malformed())).toThrow(TypeError);
+ });
+ }
+
+ it("observes changing array length and turns sparse iterator values into stored undefined", () => {
+ const { ir, native } = consumers("function copy(input: number[]): number[] { return [0, ...input, 9]; }", "copy");
+ const input = () => {
+ const values = [1];
+ Object.defineProperty(values, 0, { get() { values[1] = 2; return 1; } });
+ return values;
+ };
+
+ expect(ir(input())).toEqual(native(input()));
+ const sparse = new Array(2);
+ const result = ir(sparse);
+ expect(result).toEqual(native(sparse));
+ expect(Object.hasOwn(result, 1)).toBe(true);
+ expect(Object.hasOwn(result, 2)).toBe(true);
+ });
+
+ it("does not call next after done in a rest binding", () => {
+ const { ir, native } = consumers("function tail(input: any) { const [a, b, ...rest] = input; return rest; }", "tail");
+ const input = () => {
+ let calls = 0;
+ return { [Symbol.iterator]: () => ({ next() { if (++calls > 1) throw new Error("next after done"); return { done: true }; } }) };
+ };
+
+ expect(ir(input())).toEqual(native(input()));
+ });
+
+ it("skips an omitted binding without reading its value getter", () => {
+ const { ir, native } = consumers("function tail(input: any) { const [, ...rest] = input; return rest; }", "tail");
+ const input = () => {
+ let calls = 0;
+ return { [Symbol.iterator]: () => ({ next() {
+ if (calls++ === 0) return { done: false, get value() { throw new Error("omitted value"); } };
+ return { done: true };
+ } }) };
+ };
+
+ expect(ir(input())).toEqual(native(input()));
+ });
+
+ it("evaluates the source once and drains it before the following element", () => {
+ const source = "declare function mark(n: number): number; declare function get(): Iterable; function copy() { return [mark(0), ...get(), mark(9)]; }";
+ const observed = (consumer: "ir" | "native") => {
+ const order: any[] = [];
+ const externals = {
+ mark(n: number) { order.push(n); return n; },
+ get() {
+ order.push("source");
+ return { *[Symbol.iterator]() { order.push("first"); yield 1; order.push("second"); yield 2; order.push("done"); } };
+ },
+ };
+ const functions = consumers(source, "copy", externals);
+ return { result: functions[consumer](), order };
+ };
+
+ expect(observed("ir")).toEqual(observed("native"));
+ expect(observed("ir").order).toEqual([0, "source", "first", "second", "done", 9]);
+ });
+
+ it("closes on a default initializer throw and preserves the original exception", () => {
+ const original = new Error("binding default");
+ const { ir, native } = consumers(
+ "declare function fail(): never; function tail(input: any) { const [a = fail(), ...rest] = input; return rest; }",
+ "tail", { fail: () => { throw original; } },
+ );
+ const run = (collect: any) => {
+ let closed = 0;
+ const input = { [Symbol.iterator]: () => ({ next: () => ({ value: undefined, done: false }), return() { closed++; throw new Error("close"); } }) };
+
+ expect(() => collect(input)).toThrow(original);
+ expect(closed).toBe(1);
+ };
+
+ run(native);
+ run(ir);
+ });
+});
+
+describe("default and rest parameter observable behavior", () => {
+ it("binds earlier parameters before defaults and keeps falsy provided arguments", () => {
+ const { ir, native } = consumers("function f(a = 3, b = a + 1) { return [a, b]; }", "f");
+
+ for (const args of [[], [undefined], [0], [null], [false], [0, 7]]) {
+ expect(ir(...args)).toEqual(native(...args));
+ }
+ });
+
+ for (const source of ["function f(a = a) { return a; }", "function f(a = b, b = 2) { return a; }"]) {
+ it(`preserves the parameter temporal dead zone: ${source}`, () => {
+ const { ir, native } = consumers(source, "f");
+
+ expect(() => native()).toThrow(ReferenceError);
+ expect(() => ir()).toThrow(ReferenceError);
+ expect(ir(7)).toEqual(native(7));
+ });
+ }
+
+ it("resolves parameter defaults outside the function body's declarations", () => {
+ const { ir, native, initialize } = consumers("const seed = 7; function f(a = seed) { let seed = 9; return a; }", "f");
+ initialize();
+
+ expect(ir()).toEqual(native());
+ expect(ir()).toBe(7);
+ });
+
+ it("packs remaining raw arguments in order, including missing normal arguments", () => {
+ const { ir, native } = consumers("function f(first: number, ...rest: number[]) { return rest; }", "f");
+
+ for (const args of [[], [1], [1, 2], [1, 2, 3]]) {
+ expect(ir(...args)).toEqual(native(...args));
+ }
+ });
+});
+
+describe("regular expression intrinsic", () => {
+ it("retains escaped pattern text and flags despite a shadowed RegExp binding", () => {
+ const { ir, native } = consumers("function matcher(RegExp: any) { return /a\\/b/gi; }", "matcher");
+ const lowered = ir(() => { throw new Error("shadowed constructor"); });
+ const expected = native(() => { throw new Error("shadowed constructor"); });
+
+ expect(lowered.source).toEqual(expected.source);
+ expect(lowered.flags).toEqual(expected.flags);
+ expect(lowered.test("A/B")).toBe(true);
+ });
+});
diff --git a/jacodb-ets/ts-frontend/test/cfg.spec.ts b/jacodb-ets/ts-frontend/test/cfg.spec.ts
index 9d24f2812..9d0ed4f3b 100644
--- a/jacodb-ets/ts-frontend/test/cfg.spec.ts
+++ b/jacodb-ets/ts-frontend/test/cfg.spec.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
-import { serializeEtsFile } from "../src/serialize";
import { BasicBlockDto, MethodDto } from "../src/dto/model";
import { IfStmtDto, StmtDto } from "../src/dto/stmts";
+import { serializeEtsFile } from "../src/serialize";
import { defaultMethod, lower, methodByName } from "./util";
function blocksOf(method: MethodDto): BasicBlockDto[] {
diff --git a/jacodb-ets/ts-frontend/test/closures.spec.ts b/jacodb-ets/ts-frontend/test/closures.spec.ts
index a19a7095d..fcd0e0f7f 100644
--- a/jacodb-ets/ts-frontend/test/closures.spec.ts
+++ b/jacodb-ets/ts-frontend/test/closures.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { serializeEtsFile } from "../src/serialize";
import { AssignStmtDto, StmtDto } from "../src/dto/stmts";
+import { serializeEtsFile } from "../src/serialize";
import { validateEtsFile } from "../src/validate";
import { defaultMethod, lower, methodByName, singleBlockStmts } from "./util";
diff --git a/jacodb-ets/ts-frontend/test/lexical-this.spec.ts b/jacodb-ets/ts-frontend/test/lexical-this.spec.ts
index fe51a7dc4..5d7ca7b64 100644
--- a/jacodb-ets/ts-frontend/test/lexical-this.spec.ts
+++ b/jacodb-ets/ts-frontend/test/lexical-this.spec.ts
@@ -5,17 +5,34 @@ import { executeObjectIr } from "./object-runtime";
import { lower, methodByName } from "./util";
describe("lexical arrow receivers", () => {
- it("uses the creation receiver when a stored arrow body reads this", () => {
- const { file, diagnostics } = lower(`
- export function result() {
+ it("uses the captured receiver before evaluating an arrow parameter default", () => {
+ const source = `
+ export function result(): number {
const source = {
offset: 11,
- install: function(target: any) { target.callback = () => this.offset + 1; },
+ install: function(target: any) { target.callback = (value = this.offset) => value + 1; },
};
const target: any = { offset: 100 };
source.install(target);
return target.callback();
}
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTripped = JSON.parse(serializeEtsFile(file));
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(executeObjectIr(roundTripped, "result")).toBe(12);
+
+ const javascript = ts.transpileModule(source, {
+ compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS },
+ }).outputText;
+ expect(new Function("exports", `${javascript}\nreturn exports.result();`)({})).toBe(12);
+ });
+
+ it("preserves an outer binding referenced only by a parameter default", () => {
+ const { file, diagnostics } = lower(`
+ function make(seed: number) { return (value = seed) => value; }
+ export function result() { return make(12)(); }
`);
const roundTripped = JSON.parse(serializeEtsFile(file));
@@ -92,4 +109,22 @@ describe("lexical arrow receivers", () => {
expect(nested.every((method) => method.signature.parameters[0].type._ === "LexicalEnvType")).toBe(true);
expect(ordinary.every((method) => method.signature.parameters.length === 0)).toBe(true);
});
+ it("uses the creation receiver when a stored arrow body reads this", () => {
+ const { file, diagnostics } = lower(`
+ export function result() {
+ const source = {
+ offset: 11,
+ install: function(target: any) { target.callback = () => this.offset + 1; },
+ };
+ const target: any = { offset: 100 };
+ source.install(target);
+ return target.callback();
+ }
+ `);
+ const roundTripped = JSON.parse(serializeEtsFile(file));
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(executeObjectIr(roundTripped, "result")).toBe(12);
+ });
+
});
diff --git a/jacodb-ets/ts-frontend/test/lowering.spec.ts b/jacodb-ets/ts-frontend/test/lowering.spec.ts
index a08e9111e..b43e124a3 100644
--- a/jacodb-ets/ts-frontend/test/lowering.spec.ts
+++ b/jacodb-ets/ts-frontend/test/lowering.spec.ts
@@ -26,6 +26,25 @@ function assignmentTo(stmts: StmtDto[], name: string): AssignStmtDto | undefined
}
describe("straight-line lowering", () => {
+ it("lowers literal initializers to constants", () => {
+ const stmts = bodyStmts(`let x = 42; let s = "hi"; let b = true; let n = null; let u = undefined;`);
+ expect(stmts).toEqual([
+ asgn("x", { _: "Constant", value: "42", type: { _: "NumberType" } }),
+ asgn("s", { _: "Constant", value: "hi", type: { _: "StringType" } }),
+ asgn("b", { _: "Constant", value: "true", type: { _: "BooleanType" } }),
+ asgn("n", { _: "Constant", value: "null", type: { _: "NullType" } }),
+ asgn("u", { _: "Constant", value: "undefined", type: { _: "UndefinedType" } }),
+ ]);
+
+ function asgn(name: string, right: unknown) {
+ return {
+ _: "AssignStmt",
+ left: expect.objectContaining({ _: "StaticFieldRef", field: expect.objectContaining({ name }) }),
+ right,
+ };
+ }
+ });
+
it("preserves bigint operations and exact literals through JSON", () => {
const source = `
export function increment(x: bigint): bigint { return x + 1n; }
@@ -64,25 +83,6 @@ describe("straight-line lowering", () => {
expect(concrete).toEqual([2n, 9007199254740993n, -32n]);
});
- it("lowers literal initializers to constants", () => {
- const stmts = bodyStmts(`let x = 42; let s = "hi"; let b = true; let n = null; let u = undefined;`);
- expect(stmts).toEqual([
- asgn("x", { _: "Constant", value: "42", type: { _: "NumberType" } }),
- asgn("s", { _: "Constant", value: "hi", type: { _: "StringType" } }),
- asgn("b", { _: "Constant", value: "true", type: { _: "BooleanType" } }),
- asgn("n", { _: "Constant", value: "null", type: { _: "NullType" } }),
- asgn("u", { _: "Constant", value: "undefined", type: { _: "UndefinedType" } }),
- ]);
-
- function asgn(name: string, right: unknown) {
- return {
- _: "AssignStmt",
- left: expect.objectContaining({ _: "StaticFieldRef", field: expect.objectContaining({ name }) }),
- right,
- };
- }
- });
-
it("lowers binary expressions with immediate operands", () => {
const stmts = bodyStmts("let a = 1; let b = 2; let c = a + b;");
const c = assignmentTo(stmts, "c")!;
diff --git a/jacodb-ets/ts-frontend/test/object-computed.spec.ts b/jacodb-ets/ts-frontend/test/object-computed.spec.ts
index 8d31d4057..32447fb72 100644
--- a/jacodb-ets/ts-frontend/test/object-computed.spec.ts
+++ b/jacodb-ets/ts-frontend/test/object-computed.spec.ts
@@ -1,7 +1,7 @@
import { runInNewContext } from "node:vm";
import * as ts from "typescript";
import { describe, expect, it } from "vitest";
-import { DefineDataPropertyStmtDto } from "../src/dto/stmts";
+import { AssignStmtDto, DefineDataPropertyStmtDto } from "../src/dto/stmts";
import { compile, lower, methodByName } from "./util";
import { executeObjectIr } from "./object-runtime";
@@ -66,6 +66,58 @@ describe("computed object keys", () => {
expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "read")).toBe(8);
});
+ it("reads a computed destructuring key from the original source", () => {
+ const source = `
+ export function pick(input: Record, key: string): number {
+ const { [key]: value } = input;
+ return value;
+ }
+ `;
+ const compiled = compile(source);
+ expect(compiled.program.getSemanticDiagnostics(compiled.sourceFile)).toEqual([]);
+
+ const { file, diagnostics } = lower(source);
+ const stmts = methodByName(file, "pick").body!.cfg.blocks.flatMap((block) => block.stmts);
+
+ expect(stmts).toContainEqual(expect.objectContaining({
+ _: "AssignStmt",
+ left: expect.objectContaining({ _: "Local", name: "value" }),
+ right: expect.objectContaining({ _: "PropertyRef", key: expect.objectContaining({ _: "Local" }) }),
+ }));
+ expect(stmts).toContainEqual(expect.objectContaining({
+ _: "AssignStmt",
+ left: expect.objectContaining({ _: "Local", name: expect.stringMatching(/^%/) }),
+ right: expect.objectContaining({ _: "ToPropertyKeyExpr", arg: expect.objectContaining({ name: "key" }) }),
+ }));
+ expect(diagnostics.messages).toEqual([]);
+ expect(evaluate(source, `exports.pick({ x: 3 }, "x")`)).toBe(3);
+ expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "pick", [{ x: 3 }, "x"])).toBe(3);
+ });
+
+ it("preserves the destructuring source when a computed key mutates its binding", () => {
+ const source = `
+ let input = { x: 3 };
+ let calls = 0;
+ function change(): string { calls++; input = { x: 9 }; return "x"; }
+ export function pick(): number {
+ const { [change()]: value } = input;
+ return value + calls;
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ const stmts = methodByName(file, "pick").body!.cfg.blocks.flatMap((block) => block.stmts);
+ const callIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt"
+ && stmt.right._ === "StaticCallExpr" && stmt.right.method.name === "change");
+ const readIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "PropertyRef");
+ const read = stmts[readIndex] as AssignStmtDto;
+
+ expect(callIndex).toBeGreaterThanOrEqual(0);
+ expect(readIndex).toBeGreaterThan(callIndex);
+ expect(read.right).toMatchObject({ _: "PropertyRef", instance: { _: "Local", name: expect.stringMatching(/^%/) } });
+ expect(diagnostics.messages).toEqual([]);
+ expect(evaluate(source, `exports.pick()`)).toBe(4);
+ expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "pick")).toBe(4);
+ });
it("converts an object key before evaluating the property value", () => {
const source = `
export function read(key: any, state: { phase: string }): number {
@@ -87,6 +139,23 @@ describe("computed object keys", () => {
return exports.read(key, state);
})()`)).toBe(7);
});
+
+ it("rejects nullish binding sources before converting the key", () => {
+ const source = `export function pick(input: any, key: any) { const { [key]: value } = input; return value; }`;
+ const { file, diagnostics } = lower(source);
+ let conversions = 0;
+ const key = { toString() { conversions++; return "x"; } };
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(() => executeObjectIr(JSON.parse(JSON.stringify(file)), "pick", [null, key])).toThrow(TypeError);
+ expect(conversions).toBe(0);
+ expect(evaluate(source, `(() => {
+ let conversions = 0;
+ try { exports.pick(null, { toString() { conversions++; return "x"; } }); } catch (error) {}
+ return conversions;
+ })()`)).toBe(0);
+ });
+
it("preserves the receiver before evaluating a computed access key", () => {
const source = `export function read(key: string): number {
let input = { x: 3 };
@@ -98,4 +167,5 @@ describe("computed object keys", () => {
expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "read", ["x"])).toBe(3);
expect(evaluate(source, `exports.read("x")`)).toBe(3);
});
+
});
diff --git a/jacodb-ets/ts-frontend/test/object-copy.spec.ts b/jacodb-ets/ts-frontend/test/object-copy.spec.ts
index 3fb60fe90..d1477ab1c 100644
--- a/jacodb-ets/ts-frontend/test/object-copy.spec.ts
+++ b/jacodb-ets/ts-frontend/test/object-copy.spec.ts
@@ -1,6 +1,7 @@
import { runInNewContext } from "node:vm";
import * as ts from "typescript";
import { describe, expect, it } from "vitest";
+import { AssignStmtDto } from "../src/dto/stmts";
import { compile, lower, methodByName } from "./util";
import { executeObjectIr } from "./object-runtime";
@@ -62,6 +63,77 @@ describe("object property copying", () => {
})()`)).toBe(71);
});
+ it("builds object rest by excluding bound keys", () => {
+ const source = `
+ export function remainder(input: { x: number; y: number }): number {
+ const { x, ...rest } = input;
+ return rest.y;
+ }
+ `;
+ const compiled = compile(source);
+ expect(compiled.program.getSemanticDiagnostics(compiled.sourceFile)).toEqual([]);
+
+ const { file, diagnostics } = lower(source);
+ const stmts = methodByName(file, "remainder").body!.cfg.blocks.flatMap((block) => block.stmts);
+ const xReadIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt"
+ && stmt.right._ === "PropertyRef" && stmt.right.key._ === "Constant" && stmt.right.key.value === "x");
+ const copyIndex = stmts.findIndex((stmt) => stmt._ === "CopyDataPropertiesStmt");
+
+ expect(xReadIndex).toBeGreaterThanOrEqual(0);
+ expect(copyIndex).toBeGreaterThan(xReadIndex);
+ expect(stmts[copyIndex]).toMatchObject({
+ _: "CopyDataPropertiesStmt",
+ excludedKeys: [{ _: "Constant", value: "x" }],
+ throwOnNullishSource: true,
+ });
+ expect(diagnostics.messages).toEqual([]);
+ expect(evaluate(source, `exports.remainder({ x: 1, y: 2 })`)).toBe(2);
+ expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "remainder", [{ x: 1, y: 2 }])).toBe(2);
+ });
+
+ it("reuses one evaluated computed key for both binding and rest exclusion", () => {
+ const source = `
+ export function remainder(input: Record, key: string): number {
+ const { [key]: value, ...rest } = input;
+ return value + rest.y;
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ const stmts = methodByName(file, "remainder").body!.cfg.blocks.flatMap((block) => block.stmts);
+ const read = stmts.find((stmt): stmt is AssignStmtDto => stmt._ === "AssignStmt"
+ && stmt.right._ === "PropertyRef");
+ const copy = stmts.find((stmt) => stmt._ === "CopyDataPropertiesStmt");
+
+ expect(read?.right).toMatchObject({ _: "PropertyRef", key: { _: "Local" } });
+ expect(copy).toMatchObject({
+ _: "CopyDataPropertiesStmt",
+ excludedKeys: [(read?.right as { key: unknown }).key],
+ });
+ expect(diagnostics.messages).toEqual([]);
+ expect(evaluate(source, `exports.remainder({ x: 3, y: 2 }, "x")`)).toBe(5);
+ expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "remainder", [{ x: 3, y: 2 }, "x"])).toBe(5);
+ });
+ it("normalizes a computed key once before reading and excluding it from rest", () => {
+ const source = `
+ export function remainder(input: any, key: any): number {
+ const { [key]: value, ...rest } = input;
+ return value + rest.y;
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ let conversions = 0;
+ const key = { toString() { conversions++; return conversions === 1 ? "x" : "y"; } };
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "remainder", [{ x: 3, y: 2 }, key])).toBe(5);
+ expect(conversions).toBe(1);
+ expect(evaluate(source, `(() => {
+ let conversions = 0;
+ const key = { toString() { conversions++; return conversions === 1 ? "x" : "y"; } };
+ return exports.remainder({ x: 3, y: 2 }, key) * 10 + conversions;
+ })()`)).toBe(51);
+ });
+
it("copies enumerable symbols and excludes inherited and non-enumerable properties", () => {
const source = `export function copy(input: any): any { return { ...input }; }`;
const { file, diagnostics } = lower(source);
@@ -87,13 +159,21 @@ describe("object property copying", () => {
return Reflect.ownKeys(result).length * 10 + result[symbol] + (result.x === input.x ? 1 : 0);
})()`)).toBe(25);
});
- it("skips nullish spread sources", () => {
- const source = `export function spread(input: any): number { return { ...input, x: 7 }.x; }`;
+
+ it("skips nullish spread but rejects nullish rest", () => {
+ const source = `
+ export function spread(input: any): number { return { ...input, x: 7 }.x; }
+ export function rest(input: any): any { const { ...result } = input; return result; }
+ `;
const { file, diagnostics } = lower(source);
+ const serialized = JSON.parse(JSON.stringify(file));
expect(diagnostics.messages).toEqual([]);
- expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "spread", [null])).toBe(7);
- expect(executeObjectIr(JSON.parse(JSON.stringify(file)), "spread", [undefined])).toBe(7);
+ expect(executeObjectIr(serialized, "spread", [null])).toBe(7);
+ expect(executeObjectIr(serialized, "spread", [undefined])).toBe(7);
+ expect(() => executeObjectIr(serialized, "rest", [undefined])).toThrow(TypeError);
expect(evaluate(source, `exports.spread(null)`)).toBe(7);
+ expect(evaluate(source, `(() => { try { exports.rest(undefined); } catch (error) { return error instanceof TypeError; } })()`)).toBe(true);
});
+
});
diff --git a/jacodb-ets/ts-frontend/test/object-runtime.spec.ts b/jacodb-ets/ts-frontend/test/object-runtime.spec.ts
index bd6b444e3..4a585e41c 100644
--- a/jacodb-ets/ts-frontend/test/object-runtime.spec.ts
+++ b/jacodb-ets/ts-frontend/test/object-runtime.spec.ts
@@ -26,14 +26,14 @@ function rewriteCaptures(value: unknown, rewrite: (type: LexicalEnvTypeDto) => v
return changed;
}
-const BODY_CAPTURE = `
- function make(seed: number) { return function() { return seed; }; }
+const DEFAULT_CAPTURE = `
+ function make(seed: number) { return (value = seed) => value; }
export function result() { return make(12)(); }
`;
describe("object test interpreter closure environments", () => {
it("constructs the environment from the lifted method's declared slots", () => {
- const file = lowerSerialized(BODY_CAPTURE);
+ const file = lowerSerialized(DEFAULT_CAPTURE);
const lifted = file.classes.flatMap((clazz) => clazz.methods)
.find((method) => method.signature.parameters[0]?.type._ === "LexicalEnvType")!;
const environmentType = lifted.signature.parameters[0].type;
@@ -46,7 +46,7 @@ describe("object test interpreter closure environments", () => {
});
it("rejects a closure reference omitted from its base's declared slots", () => {
- const file = lowerSerialized(BODY_CAPTURE);
+ const file = lowerSerialized(DEFAULT_CAPTURE);
const reference = file.classes.flatMap((clazz) => clazz.methods)
.flatMap((method) => method.body?.cfg.blocks.flatMap((block) => block.stmts) ?? [])
.flatMap((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ClosureFieldRef" ? [stmt.right] : [])
diff --git a/jacodb-ets/ts-frontend/test/object-runtime.ts b/jacodb-ets/ts-frontend/test/object-runtime.ts
index fb265b41f..475b0d31d 100644
--- a/jacodb-ets/ts-frontend/test/object-runtime.ts
+++ b/jacodb-ets/ts-frontend/test/object-runtime.ts
@@ -90,7 +90,6 @@ export function executeObjectIr(file: EtsFileDto, name: string, args: unknown[]
if (source === null || source === undefined) throw new TypeError("nullish object binding");
return source;
}
-
case "StaticCallExpr": return invoke(lookup(value.method), value.args.map(read), undefined);
case "InstanceCallExpr": {
const object = read(value.instance);
diff --git a/jacodb-ets/ts-frontend/test/regressions.spec.ts b/jacodb-ets/ts-frontend/test/regressions.spec.ts
index 4a3ed8da2..5016214f3 100644
--- a/jacodb-ets/ts-frontend/test/regressions.spec.ts
+++ b/jacodb-ets/ts-frontend/test/regressions.spec.ts
@@ -1,5 +1,7 @@
import { describe, expect, it } from "vitest";
+import { executor } from "./execute";
import { AssignStmtDto, StmtDto } from "../src/dto/stmts";
+import { serializeEtsFile } from "../src/serialize";
import { syntaxKindName } from "../src/lowering/diagnostics";
import * as ts from "typescript";
import { validateEtsFile } from "../src/validate";
@@ -12,9 +14,9 @@ function allStmts(method: { body?: { cfg: { blocks: { stmts: StmtDto[] }[] } } }
describe("unsupported loop bindings do not break the whole file", () => {
// `lower()` throws on unplaced labels via finalize(), so merely lowering is the assertion.
const cases: Record = {
- "array pattern with rest": "declare const xs: any[]; for (const [a, ...rest] of xs) { console.log(a); }",
+ "array pattern with rest": "declare const xs: any[]; for (const [[a], ...rest] of xs) { console.log(a); }",
"member expression target": "declare const xs: any[]; const obj: any = {}; for (obj.x of xs) { console.log(obj.x); }",
- "rest in for-in": "declare const o: any; for (const [a, ...rest] in o) { console.log(a); }",
+ "rest in for-in": "declare const o: any; for (const [[a], ...rest] in o) { console.log(a); }",
};
for (const [name, source] of Object.entries(cases)) {
@@ -94,6 +96,203 @@ describe("pattern parameters", () => {
});
});
+describe("default parameter initializers", () => {
+ it("captures outer values referenced only by a closure's parameter default", () => {
+ const { file, diagnostics } = lower("function wrap(seed: number) { return (value = seed) => value; }");
+ const closure = methodByName(file, "%AM0$wrap");
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(allStmts(closure).some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ClosureFieldRef"
+ && stmt.right.fieldName === "seed")).toBe(true);
+ });
+
+ it("keeps delayed temporal-dead-zone captures explicitly unsupported", () => {
+ const { file, diagnostics } = lower("function f(a = () => b, b = a()) { return b; }");
+
+ expect(diagnostics.messages.some((message) => message.includes("runtime temporal-dead-zone cell"))).toBe(true);
+ expect(allStmts(methodByName(file, "f")).some((stmt) => stmt._ === "AssignStmt"
+ && stmt.right._ === "UnsupportedValue")).toBe(true);
+ });
+
+ it("guards defaults after their own argument reads in source order through JSON", () => {
+ const source = `
+ const order: string[] = [];
+ function record(label: string, value: number): number { order.push(label); return value; }
+ function value(x = record("x", 3), y = record("y", 4)): number { return x + y; }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const body = methodByName(roundTrip, "value").body!;
+ const stmts = allStmts({ body });
+ const argumentReads = stmts.flatMap((stmt, index) =>
+ stmt._ === "AssignStmt" && stmt.right._ === "ParameterRef"
+ ? [{ index, parameterIndex: stmt.right.index }]
+ : [],
+ );
+ const defaults = stmts.flatMap((stmt, index) =>
+ stmt._ === "AssignStmt" && stmt.right._ === "StaticCallExpr" && stmt.right.method.name === "record"
+ ? [{ index, label: stmt.right.args[0] }]
+ : [],
+ );
+ const conditions = stmts.filter((stmt) => stmt._ === "IfStmt").map((stmt) => stmt.condition);
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(argumentReads.map((read) => read.parameterIndex)).toEqual([0, 1]);
+ expect(defaults.map((call) => call.label)).toMatchObject([
+ { _: "Constant", value: "x" },
+ { _: "Constant", value: "y" },
+ ]);
+ expect(argumentReads[0].index).toBeLessThan(defaults[0].index);
+ expect(defaults[0].index).toBeLessThan(argumentReads[1].index);
+ expect(argumentReads[1].index).toBeLessThan(defaults[1].index);
+ expect(conditions).toEqual([
+ expect.objectContaining({ _: "ConditionExpr", op: "===", right: { _: "Constant", value: "undefined", type: { _: "UndefinedType" } } }),
+ expect.objectContaining({ _: "ConditionExpr", op: "===", right: { _: "Constant", value: "undefined", type: { _: "UndefinedType" } } }),
+ ]);
+
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const concrete = new Function(`${js}\nreturn {
+ missing: value(),
+ undefinedValue: value(undefined, undefined),
+ zero: value(0, 1),
+ order,
+ };`)() as { missing: number; undefinedValue: number; zero: number; order: string[] };
+ expect(concrete).toEqual({ missing: 7, undefinedValue: 7, zero: 1, order: ["x", "y", "x", "y"] });
+ const ir = executor(roundTrip);
+ ir.initialize();
+ expect([ir.call("value"), ir.call("value", undefined, undefined), ir.call("value", 0, 1)]).toEqual([7, 7, 1]);
+ });
+});
+
+describe("rest parameter calls", () => {
+ it("preserves the rest signature and array-valued argument slot through JSON", () => {
+ const source = `
+ function count(...values: number[]): number { return values.length; }
+ function sample(): number { return count() + count(1, 2); }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const method = methodByName(roundTrip, "count");
+ const stmts = allStmts(method);
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(method.signature.parameters).toMatchObject([
+ { name: "values", type: { _: "ArrayType" }, isRest: true },
+ ]);
+ expect(stmts).toContainEqual(expect.objectContaining({
+ _: "AssignStmt",
+ left: expect.objectContaining({ _: "Local", name: "values", type: expect.objectContaining({ _: "ArrayType" }) }),
+ right: expect.objectContaining({ _: "ParameterRef", index: 0, type: expect.objectContaining({ _: "ArrayType" }) }),
+ }));
+ const callArgs = allStmts(methodByName(roundTrip, "sample"))
+ .filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "StaticCallExpr"
+ && stmt.right.method.name === "count")
+ .map((stmt) => stmt.right.args.length);
+ expect(callArgs).toEqual([0, 2]);
+
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const concrete = new Function(`${js}\nreturn [count(), count(1, 2), sample()];`)() as number[];
+ expect(concrete).toEqual([0, 2, 2]);
+ const ir = executor(roundTrip);
+ expect([ir.call("count"), ir.call("count", 1, 2), ir.call("sample")]).toEqual(concrete);
+ });
+});
+
+describe("array rest bindings", () => {
+ it("copies the tail into a fresh array for empty, short, and long inputs through JSON", () => {
+ const source = `
+ function tail(input: number[]): number[] {
+ const [first, ...rest] = input;
+ return rest;
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = allStmts(methodByName(roundTrip, "tail"));
+ const allocations = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewArrayExpr");
+ const iteratorCalls = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "InstanceCallExpr"
+ && stmt.right.method.name === "Symbol.iterator");
+ const tailStores = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.left._ === "ArrayRef"
+ && stmt.left.array._ === "Local" && stmt.left.array.name !== "input");
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(allocations).toHaveLength(1);
+ expect(allocations[0]).toMatchObject({ right: { _: "NewArrayExpr", size: { _: "Constant", value: "0" } } });
+ expect(iteratorCalls).toHaveLength(1);
+ expect(tailStores).toHaveLength(1);
+ expect(stmts.filter((stmt) => stmt._ === "IfStmt").length).toBeGreaterThan(2);
+ expect(stmts.some((stmt) => stmt._ === "UnsupportedStmt")).toBe(false);
+
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const concrete = new Function(`${js}\nreturn [tail([]), tail([1]), tail([1, 2, 3])];`)() as number[][];
+ expect(concrete).toEqual([[], [], [2, 3]]);
+ const ir = executor(roundTrip);
+ expect([ir.call("tail", []), ir.call("tail", [1]), ir.call("tail", [1, 2, 3])]).toEqual(concrete);
+ });
+});
+
+describe("array spread literals", () => {
+ it("copies a variable-length input in order through JSON", () => {
+ const source = `
+ function copy(input: number[]): number[] { return [0, ...input, 9]; }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = allStmts(methodByName(roundTrip, "copy"));
+ const allocations = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewArrayExpr");
+ const lengthRead = stmts.find((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "InstanceFieldRef"
+ && stmt.right.field.name === "length");
+ const sourceRead = stmts.find((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ArrayRef"
+ && stmt.right.array._ === "Local" && stmt.right.array.name !== "input");
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(allocations).toHaveLength(1);
+ expect(allocations[0]).toMatchObject({ right: { _: "NewArrayExpr", size: { _: "Constant", value: "0" } } });
+ expect(lengthRead).toBeUndefined();
+ expect(sourceRead).toBeUndefined();
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "InstanceCallExpr"
+ && stmt.right.method.name === "Symbol.iterator")).toBe(true);
+ expect(stmts.some((stmt) => stmt._ === "UnsupportedValue")).toBe(false);
+
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const concrete = new Function(`${js}\nreturn [copy([]), copy([1]), copy([1, 2, 3])];`)() as number[][];
+ expect(concrete).toEqual([[0, 9], [0, 1, 9], [0, 1, 2, 3, 9]]);
+ const ir = executor(roundTrip);
+ expect([ir.call("copy", []), ir.call("copy", [1]), ir.call("copy", [1, 2, 3])]).toEqual(concrete);
+ });
+});
+
+describe("regular-expression literals", () => {
+ it("retains pattern, flags, allocation, and test call through JSON", () => {
+ const source = `function containsX(value: string): boolean { return /x+/gi.test(value); }`;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = allStmts(methodByName(roundTrip, "containsX"));
+ const allocations = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewExpr");
+ const calls = stmts.filter((stmt) => stmt._ === "AssignStmt"
+ && (stmt.right._ === "InstanceCallExpr" || stmt.right._ === "PtrCallExpr"));
+
+ expect(diagnostics.messages).toEqual([]);
+ expect(allocations).toHaveLength(1);
+ expect(calls).toHaveLength(2);
+ expect(calls[0]).toMatchObject({
+ right: {
+ method: { name: "constructor" },
+ args: [{ _: "Constant", value: "x+" }, { _: "Constant", value: "gi" }],
+ },
+ });
+ expect(calls[1]).toMatchObject({
+ right: { _: "PtrCallExpr", method: { name: "test" }, receiver: { _: "Local" }, ptr: { _: "Local" } },
+ });
+
+ const js = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020 } }).outputText;
+ const concrete = new Function(`${js}\nreturn [containsX("XX"), containsX("ab")];`)() as boolean[];
+ expect(concrete).toEqual([true, false]);
+ const ir = executor(roundTrip);
+ expect([ir.call("containsX", "XX"), ir.call("containsX", "ab")]).toEqual(concrete);
+ });
+});
+
describe("source local names", () => {
it("keeps source parameters out of validator-reserved local prefixes", () => {
const { file } = lower("function f(_tmp0: number) { return _tmp0 + 1; }");
diff --git a/jacodb-ets/ts-frontend/test/spread-calls.spec.ts b/jacodb-ets/ts-frontend/test/spread-calls.spec.ts
new file mode 100644
index 000000000..75434391c
--- /dev/null
+++ b/jacodb-ets/ts-frontend/test/spread-calls.spec.ts
@@ -0,0 +1,216 @@
+import * as ts from "typescript";
+import { describe, expect, it } from "vitest";
+import { StmtDto } from "../src/dto/stmts";
+import { serializeEtsFile } from "../src/serialize";
+import { lower, methodByName, singleBlockStmts } from "./util";
+
+function assertSpreadArgumentOrder(stmts: StmtDto[], methodName: string): void {
+ const expansion = stmts.find((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "SpreadExpansionExpr");
+ const reads = stmts.filter((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ArrayRef");
+ const call = stmts.find((stmt) => stmt._ === "AssignStmt"
+ && (stmt.right._ === "StaticCallExpr" || stmt.right._ === "InstanceCallExpr")
+ && stmt.right.method.name === methodName);
+
+ expect(reads).toHaveLength(2);
+ expect(expansion).toMatchObject({ right: { _: "SpreadExpansionExpr", expectedCount: 2 } });
+ for (const read of reads) {
+ expect(read).toMatchObject({ right: { array: expansion?._ === "AssignStmt" ? expansion.left : undefined } });
+ }
+ expect(reads.map((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ArrayRef"
+ && stmt.right.index._ === "Constant" ? stmt.right.index.value : "invalid")).toEqual(["0", "1"]);
+ expect(call).toMatchObject({
+ _: "AssignStmt",
+ right: {
+ args: reads.map((stmt) => stmt._ === "AssignStmt" ? stmt.left : undefined),
+ },
+ });
+}
+
+function runTypeScript(source: string, expression: string): unknown {
+ const js = ts.transpileModule(source, {
+ compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS },
+ }).outputText;
+
+ return new Function("exports", `${js}\nreturn ${expression};`)({});
+}
+
+describe("fixed-size spread arguments", () => {
+ it("captures a computed method value and receiver before expanding its spread", () => {
+ const source = `
+ class Sum {
+ bias = 10;
+ add(a: number, b: number): number { return this.bias + a + b; }
+ }
+ export function value(receiver: Sum, pair: [number, number]): number {
+ return receiver["add"](...pair);
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ const stmts = singleBlockStmts(methodByName(JSON.parse(serializeEtsFile(file)), "value"));
+ const property = stmts.find((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "PropertyRef");
+ const expansionIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "SpreadExpansionExpr");
+ const call = stmts.find((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "PtrCallExpr");
+
+ expect(property).toBeDefined();
+ expect(expansionIndex).toBeGreaterThan(stmts.indexOf(property!));
+ expect(call).toMatchObject({ right: {
+ ptr: property?._ === "AssignStmt" ? property.left : undefined,
+ receiver: property?._ === "AssignStmt" && property.right._ === "PropertyRef" ? property.right.instance : undefined,
+ args: expect.any(Array),
+ } });
+ expect(call?._ === "AssignStmt" && call.right._ === "PtrCallExpr" ? call.right.args : []).toHaveLength(2);
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "exports.value(new Sum(), [2, 3])")).toBe(15);
+ });
+
+ it("preserves runtime iteration for a tuple with a replaced iterator", () => {
+ const source = `
+ function add(a: number, b: number): number { return a + b; }
+ export function sum(pair: [number, number]): number { return add(...pair); }
+ export function concrete(): number {
+ const pair: [number, number] = [2, 3];
+ pair[Symbol.iterator] = function* () { yield 7; yield 8; };
+ return sum(pair);
+ }
+ `;
+ const { file } = lower(source);
+ const stmts = singleBlockStmts(methodByName(JSON.parse(serializeEtsFile(file)), "sum"));
+
+ assertSpreadArgumentOrder(stmts, "add");
+ expect(runTypeScript(source, "exports.concrete()")).toBe(15);
+ });
+
+ it("expands a tuple in an ordinary call and preserves JSON argument order", () => {
+ const source = `
+ function add(a: number, b: number): number { return a + b; }
+ export function sum(pair: [number, number]): number { return add(...pair); }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = singleBlockStmts(methodByName(roundTrip, "sum"));
+
+ assertSpreadArgumentOrder(stmts, "add");
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "exports.sum([2, 3])")).toBe(5);
+ });
+
+ it("expands a tuple in a constructor call and preserves JSON argument order", () => {
+ const source = `
+ class Point {
+ y: number;
+ constructor(x: number, y: number) { this.y = y; }
+ }
+ export function make(pair: [number, number]): number { return new Point(...pair).y; }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = singleBlockStmts(methodByName(roundTrip, "make"));
+
+ assertSpreadArgumentOrder(stmts, "constructor");
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "exports.make([2, 3])")).toBe(3);
+ });
+
+ it("evaluates arguments around a spread in source order", () => {
+ const source = `
+ let events: string[] = [];
+ function first(): number { events.push("first"); return 1; }
+ function pair(): [number, number] { events.push("pair"); return [2, 3]; }
+ function last(): number { events.push("last"); return 4; }
+ function combine(a: number, b: number, c: number, d: number): number {
+ return a * 1000 + b * 100 + c * 10 + d;
+ }
+ export function value(): number { return combine(first(), ...pair(), last()); }
+ export function trace(): string { return events.join(","); }
+ `;
+ const { file, diagnostics } = lower(source);
+ const stmts = singleBlockStmts(methodByName(file, "value"));
+ const calls = stmts.flatMap((stmt, index) => stmt._ === "AssignStmt"
+ && stmt.right._ === "StaticCallExpr" ? [{ name: stmt.right.method.name, index, args: stmt.right.args }] : []);
+ const reads = stmts.flatMap((stmt, index) => stmt._ === "AssignStmt"
+ && stmt.right._ === "ArrayRef" ? [index] : []);
+
+ expect(calls.map((call) => call.name)).toEqual(["first", "pair", "last", "combine"]);
+ expect(reads).toHaveLength(2);
+ expect(calls[1]!.index).toBeLessThan(reads[0]);
+ expect(reads[1]).toBeLessThan(calls[2]!.index);
+ expect(calls[3]!.args).toHaveLength(4);
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "{ value: exports.value(), trace: exports.trace() }")).toEqual({
+ value: 1234,
+ trace: "first,pair,last",
+ });
+ });
+
+ it("rejects an unbounded spread without calling the target", () => {
+ const source = `
+ function add(a: number, b: number): number { return a + b; }
+ class Point { constructor(public x: number, public y: number) {} }
+ export function sum(values: number[]): number { return add(...values); }
+ export function make(values: number[]): Point { return new Point(...values); }
+ `;
+ const { file, diagnostics } = lower(source);
+ const stmts = singleBlockStmts(methodByName(file, "sum"));
+
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "StaticCallExpr"
+ && stmt.right.method.name === "add")).toBe(false);
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "UnsupportedValue"
+ && stmt.right.kindName === "CallExpression")).toBe(true);
+ expect(diagnostics.messages).toEqual(expect.arrayContaining([
+ expect.stringContaining("spread argument has no statically known length"),
+ ]));
+
+ const constructorStmts = singleBlockStmts(methodByName(file, "make"));
+ expect(constructorStmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewExpr")).toBe(false);
+ expect(constructorStmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "UnsupportedValue"
+ && stmt.right.kindName === "NewExpression")).toBe(true);
+ });
+});
+
+describe("computed constructor arrays", () => {
+ it("keeps optional computed object constructors guarded and uses property semantics", () => {
+ const source = `
+ class A { x = 1; }
+ export function make(holder: { Ctor: typeof A } | undefined): number {
+ return new (holder?.["Ctor"])().x;
+ }
+ `;
+ const { file, diagnostics } = lower(source);
+ const method = methodByName(JSON.parse(serializeEtsFile(file)), "make");
+ const stmts = method.body!.cfg.blocks.flatMap((block) => block.stmts);
+
+ expect(method.body!.cfg.blocks.length).toBeGreaterThan(1);
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "PropertyRef")).toBe(true);
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ArrayRef")).toBe(false);
+ expect(stmts.some((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewExpr"
+ && stmt.right.constructorValue?._ === "Local")).toBe(true);
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "exports.make({ Ctor: A })")).toBe(1);
+ });
+
+ it("reads an indexed constructor once before allocation and preserves JSON", () => {
+ const source = `
+ class A { kind = "A"; }
+ class B { kind = "B"; }
+ let reads = 0;
+ function constructors(): Array { reads++; return [A, B]; }
+ function index(): number { reads++; return 1; }
+ export function make(): string { return new (constructors()[index()])().kind; }
+ export function count(): number { return reads; }
+ `;
+ const { file, diagnostics } = lower(source);
+ const roundTrip = JSON.parse(serializeEtsFile(file));
+ const stmts = singleBlockStmts(methodByName(roundTrip, "make"));
+ const functionCalls = stmts.flatMap((stmt, index) => stmt._ === "AssignStmt"
+ && stmt.right._ === "StaticCallExpr" ? [{ name: stmt.right.method.name, index }] : []);
+ const readIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "ArrayRef");
+ const allocationIndex = stmts.findIndex((stmt) => stmt._ === "AssignStmt" && stmt.right._ === "NewExpr"
+ && stmt.right.constructorValue?._ === "Local");
+
+ expect(functionCalls.map((call) => call.name)).toEqual(["constructors", "index"]);
+ expect(functionCalls[1]!.index).toBeLessThan(readIndex);
+ expect(readIndex).toBeLessThan(allocationIndex);
+ expect(diagnostics.messages).toEqual([]);
+ expect(runTypeScript(source, "{ kind: exports.make(), count: exports.count() }")).toEqual({ kind: "B", count: 2 });
+ });
+});
diff --git a/jacodb-ets/ts-frontend/test/types.spec.ts b/jacodb-ets/ts-frontend/test/types.spec.ts
index 7e72bb9ff..2aef33dc9 100644
--- a/jacodb-ets/ts-frontend/test/types.spec.ts
+++ b/jacodb-ets/ts-frontend/test/types.spec.ts
@@ -342,8 +342,8 @@ describe("typeOfNode (inference)", () => {
expect(inferredOf("let x = 42;")).toEqual({ _: "NumberType" });
expect(inferredOf(`let x = "s";`)).toEqual({ _: "StringType" });
expect(inferredOf("let x = true;")).toEqual({ _: "BooleanType" });
- expect(inferredOf("let x = 1n;")).toEqual({ _: "BigIntType" });
expect(inferredOf("let x = Symbol();")).toEqual({ _: "SymbolType" });
+ expect(inferredOf("let x = 1n;")).toEqual({ _: "BigIntType" });
});
it("keeps literal types for const-declarations", () => {