Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import org.jacodb.ets.model.EtsAndExpr
import org.jacodb.ets.model.EtsAnyType
import org.jacodb.ets.model.EtsArrayAccess
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsDefineAccessorStmt
import org.jacodb.ets.model.EtsCopyDataPropertiesStmt
import org.jacodb.ets.model.EtsAwaitExpr
import org.jacodb.ets.model.EtsBitAndExpr
import org.jacodb.ets.model.EtsBitNotExpr
Expand Down Expand Up @@ -287,6 +289,25 @@ class EtsMethodBuilder(
value = ensureOneAddress(value.toEtsEntity()),
)

is CopyDataPropertiesStmtDto -> {
val targetLocal = target as? LocalDto
?: error("CopyDataPropertiesStmt target must be LocalDto")
EtsCopyDataPropertiesStmt(
location = loc(),
target = targetLocal.toEtsLocal(),
source = ensureOneAddress(source.toEtsEntity()),
excludedKeys = excludedKeys.map { ensureOneAddress(it.toEtsEntity()) },
throwOnNullishSource = throwOnNullishSource,
)
}

is DefineAccessorStmtDto -> EtsDefineAccessorStmt(
location = loc(),
target = (target as LocalDto).toEtsLocal(),
key = ensureOneAddress(key.toEtsEntity()),
getter = ensureOneAddress(getter.toEtsEntity()),
)

is CallStmtDto -> {
val expr = expr.toEtsEntity()
if (expr !is EtsCallExpr) {
Expand Down Expand Up @@ -596,7 +617,7 @@ class EtsMethodBuilder(

private fun EtsStmt.mayThrow(): Boolean = when (this) {
is EtsAssignStmt -> lhv !is EtsLocal || rhv.mayThrow()
is EtsDefineDataPropertyStmt, is EtsCallStmt, is EtsThrowStmt -> true
is EtsDefineDataPropertyStmt, is EtsCopyDataPropertiesStmt, is EtsDefineAccessorStmt, is EtsCallStmt, is EtsThrowStmt -> true
is EtsRawStmt -> true
is EtsIfStmt, is EtsNopStmt, is EtsReturnStmt -> false
else -> error("Unknown statement: ${this::class.java}")
Expand Down
18 changes: 18 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Stmts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ data class DefineDataPropertyStmtDto(
val value: ValueDto,
) : StmtDto

/** ECMAScript CopyDataProperties, including ordered own enumerable keys. */
@Serializable
@SerialName("CopyDataPropertiesStmt")
data class CopyDataPropertiesStmtDto(
val target: ValueDto, // Local; polymorphic JSON includes its discriminator.
val source: ValueDto,
val excludedKeys: List<ValueDto>,
val throwOnNullishSource: Boolean,
) : StmtDto

@Serializable
@SerialName("DefineAccessorStmt")
data class DefineAccessorStmtDto(
val target: ValueDto,
val key: ValueDto,
val getter: ValueDto,
) : StmtDto

@Serializable
@SerialName("CallStmt")
data class CallStmtDto(
Expand Down
47 changes: 47 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Stmt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ interface EtsStmt : CommonInst {
if (this is Default) return defaultVisit(stmt)
error("Cannot handle ${stmt::class.java.simpleName}: $stmt")
}
fun visit(stmt: EtsCopyDataPropertiesStmt): R {
if (this is Default) return defaultVisit(stmt)
error("Cannot handle ${stmt::class.java.simpleName}: $stmt")
}
fun visit(stmt: EtsDefineAccessorStmt): R {
if (this is Default) return defaultVisit(stmt)
error("Cannot handle ${stmt::class.java.simpleName}: $stmt")
}
fun visit(stmt: EtsReturnStmt): R
fun visit(stmt: EtsThrowStmt): R
fun visit(stmt: EtsIfStmt): R
Expand All @@ -48,6 +56,8 @@ interface EtsStmt : CommonInst {
override fun visit(stmt: EtsNopStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsAssignStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsDefineDataPropertyStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsCopyDataPropertiesStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsDefineAccessorStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsReturnStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsThrowStmt): R = defaultVisit(stmt)
override fun visit(stmt: EtsIfStmt): R = defaultVisit(stmt)
Expand Down Expand Up @@ -115,6 +125,43 @@ data class EtsDefineDataPropertyStmt(
override fun <R> accept(visitor: EtsStmt.Visitor<R>): R = visitor.visit(this)
}

/**
* Copy each own enumerable property of [source] to [target] in ECMAScript key
* enumeration order, reading values through Get and creating data properties.
* Keys in [excludedKeys] have already been evaluated and converted to property
* keys before this statement. A nullish source is ignored for spread and rejected for
* object rest according to [throwOnNullishSource].
*/
data class EtsCopyDataPropertiesStmt(
override val location: EtsStmtLocation,
val target: EtsLocal,
val source: EtsValue,
val excludedKeys: List<EtsValue>,
val throwOnNullishSource: Boolean,
) : EtsStmt {
override fun toString(): String =
"copyDataProperties($target, $source, ${excludedKeys.joinToString(prefix = "[", postfix = "]")})"

override fun <R> accept(visitor: EtsStmt.Visitor<R>): R = visitor.visit(this)
}

/**
* Define an own enumerable, configurable getter descriptor. Property Get invokes
* [getter] with [target] as its dynamic receiver. Creating it does not invoke the body;
* an existing setter component is retained, while a data descriptor is replaced.
* [key] has already been converted by ToPropertyKey.
*/
data class EtsDefineAccessorStmt(
override val location: EtsStmtLocation,
val target: EtsLocal,
val key: EtsValue,
val getter: EtsValue,
) : EtsStmt {
override fun toString(): String = "defineAccessor($target, $key, $getter)"

override fun <R> accept(visitor: EtsStmt.Visitor<R>): R = visitor.visit(this)
}

interface EtsTerminatingStmt : EtsStmt

data class EtsReturnStmt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import org.jacodb.ets.model.EtsAddExpr
import org.jacodb.ets.model.EtsAndExpr
import org.jacodb.ets.model.EtsArrayAccess
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsDefineAccessorStmt
import org.jacodb.ets.model.EtsCopyDataPropertiesStmt
import org.jacodb.ets.model.EtsAwaitExpr
import org.jacodb.ets.model.EtsBitAndExpr
import org.jacodb.ets.model.EtsBitNotExpr
Expand Down Expand Up @@ -112,6 +114,12 @@ private object StmtGetOperands : EtsStmt.Visitor<Sequence<EtsEntity>> {
override fun visit(stmt: EtsDefineDataPropertyStmt): Sequence<EtsEntity> =
sequenceOf(stmt.target, stmt.key, stmt.value)

override fun visit(stmt: EtsCopyDataPropertiesStmt): Sequence<EtsEntity> =
sequenceOf(stmt.target, stmt.source) + stmt.excludedKeys.asSequence()

override fun visit(stmt: EtsDefineAccessorStmt): Sequence<EtsEntity> =
sequenceOf(stmt.target, stmt.key, stmt.getter)

override fun visit(stmt: EtsCallStmt): Sequence<EtsEntity> =
sequenceOf(stmt.expr)

Expand Down
16 changes: 16 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/utils/Handler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import org.jacodb.ets.model.EtsAddExpr
import org.jacodb.ets.model.EtsAndExpr
import org.jacodb.ets.model.EtsArrayAccess
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsDefineAccessorStmt
import org.jacodb.ets.model.EtsCopyDataPropertiesStmt
import org.jacodb.ets.model.EtsAwaitExpr
import org.jacodb.ets.model.EtsBitAndExpr
import org.jacodb.ets.model.EtsBitNotExpr
Expand Down Expand Up @@ -140,6 +142,20 @@ abstract class AbstractHandler : EtsEntity.Visitor<Unit>, EtsStmt.Visitor<Unit>
stmt.value.accept(this)
}

final override fun visit(stmt: EtsCopyDataPropertiesStmt) {
handle(stmt)
stmt.target.accept(this)
stmt.source.accept(this)
stmt.excludedKeys.forEach { it.accept(this) }
}

final override fun visit(stmt: EtsDefineAccessorStmt) {
handle(stmt)
stmt.target.accept(this)
stmt.key.accept(this)
stmt.getter.accept(this)
}

final override fun visit(stmt: EtsCallStmt) {
handle(stmt)
stmt.expr.accept(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ 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.BooleanTypeDto
import org.jacodb.ets.dto.CaughtExceptionRefDto
import org.jacodb.ets.dto.ClassTypeDto
Expand Down Expand Up @@ -53,6 +57,9 @@ 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.EtsCallStmt
import org.jacodb.ets.model.EtsClassValueRef
Expand Down Expand Up @@ -86,6 +93,7 @@ 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

/**
Expand Down Expand Up @@ -116,6 +124,66 @@ class EtsTsFrontendTest {
}
}

@Test
fun `object getter descriptor and lifted body survive JSON conversion`() {
val frontendDto = runFrontend(
"""
export function value(seed: number): number {
const object = { y: 2, get x() { return seed + this.y; } };
return object.x;
}
""".trimIndent(),
)

val serialized = Json { serializersModule = dtoModule }.encodeToString(frontendDto)
val roundTripped = EtsFileDto.loadFromJson(serialized)
val dtoMethods = roundTripped.classes.flatMap { it.methods }
val descriptor = dtoMethods.single { it.signature.name == "value" }
.body!!.cfg.blocks.flatMap { it.stmts }.filterIsInstance<DefineAccessorStmtDto>().single()
val getterType = (descriptor.getter as LocalDto).type as FunctionTypeDto
val getterBody = dtoMethods.single { it.signature.name == getterType.signature.name }.body!!

assertEquals("x", (descriptor.key as ConstantDto).value)
assertTrue(getterBody.cfg.blocks.flatMap { it.stmts }.any { it is ReturnStmtDto })
assertTrue(getterBody.locals.any { it.type is LexicalEnvTypeDto })

val scene = EtsScene(listOf(roundTripped.toEtsFile()))
val method = scene.projectClasses.flatMap { it.methods }.single { it.name == "value" }
val converted = method.cfg.stmts.filterIsInstance<EtsDefineAccessorStmt>().single()
assertEquals("x", (converted.key as EtsStringConstant).value)
assertTrue(method.cfg.stmts.filterIsInstance<EtsAssignStmt>().any { it.rhv is EtsPropertyRef })
}

@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<CopyDataPropertiesStmtDto>()

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<EtsCopyDataPropertiesStmt>()
assertEquals(1, converted.size)
assertTrue(converted[0].excludedKeys.isEmpty())
}

@Test
fun `computed object keys survive the frontend JSON round trip`() {
val frontendDto = runFrontend(
Expand Down
19 changes: 19 additions & 0 deletions jacodb-ets/ts-frontend/src/dto/stmts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { CallExprDto, ImmediateDto, LocalDto, LValueDto, ValueDto } from "./valu
export type StmtDto =
| NopStmtDto
| AssignStmtDto
| DefineAccessorStmtDto
| CopyDataPropertiesStmtDto
| DefineDataPropertyStmtDto
| CallStmtDto
| ReturnVoidStmtDto
Expand Down Expand Up @@ -63,6 +65,23 @@ export interface DefineDataPropertyStmtDto {
value: ImmediateDto;
}

/** ECMAScript CopyDataProperties: own enumerable keys in source enumeration order. */
export interface CopyDataPropertiesStmtDto {
readonly _: "CopyDataPropertiesStmt";
target: LocalDto;
source: ImmediateDto;
excludedKeys: ImmediateDto[];
throwOnNullishSource: boolean;
}

/** Define an own enumerable configurable getter descriptor, without invoking an existing setter. */
export interface DefineAccessorStmtDto {
readonly _: "DefineAccessorStmt";
target: LocalDto;
key: ImmediateDto;
getter: ImmediateDto;
}

export interface CallStmtDto {
readonly _: "CallStmt";
expr: CallExprDto;
Expand Down
6 changes: 2 additions & 4 deletions jacodb-ets/ts-frontend/src/lowering/astUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,11 @@ export function usesModuleLexicalThis(expression: ts.Expression): boolean {
return visit(expression);
}

/** Computed method and accessor names still lack a runtime definition. */
/** Computed method and setter names still lack a runtime definition. */
export function hasUnsupportedComputedObjectKey(expression: ts.Expression): boolean {
const visit = (node: ts.Node): boolean => {
if (ts.isObjectLiteralExpression(node) && node.properties.some((property) =>
(ts.isMethodDeclaration(property)
|| ts.isGetAccessorDeclaration(property)
|| ts.isSetAccessorDeclaration(property))
(ts.isMethodDeclaration(property) || ts.isSetAccessorDeclaration(property))
&& ts.isComputedPropertyName(property.name),
)) return true;

Expand Down
Loading