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
14 changes: 9 additions & 5 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -530,11 +530,7 @@ class EtsMethodBuilder(
args = args.map { ensureLocal(it.toEtsEntity()) },
type = type.toEtsType(),
isSuperCall = isSuperCall,
wellKnownSymbol = when (wellKnownSymbol) {
null -> null
"iterator" -> EtsWellKnownSymbol.ITERATOR
else -> error("Unknown intrinsic symbol key: $wellKnownSymbol")
},
wellKnownSymbol = wellKnownSymbol.toEtsWellKnownSymbol(),
)

is StaticCallExprDto -> EtsStaticCallExpr(
Expand Down Expand Up @@ -861,6 +857,7 @@ fun FieldSignatureDto.toEtsFieldSignature(): EtsFieldSignature {
enclosingClass = declaringClass.toEtsClassSignature(),
name = name,
type = type.toEtsType(),
wellKnownSymbol = wellKnownSymbol.toEtsWellKnownSymbol(),
)
}

Expand All @@ -878,9 +875,16 @@ fun MethodSignatureDto.toEtsMethodSignature(): EtsMethodSignature {
)
},
returnType = returnType.toEtsType(),
wellKnownSymbol = wellKnownSymbol.toEtsWellKnownSymbol(),
)
}

private fun String?.toEtsWellKnownSymbol(): EtsWellKnownSymbol? = when (this) {
null -> null
"iterator" -> EtsWellKnownSymbol.ITERATOR
else -> error("Unknown intrinsic symbol key: $this")
}

fun LocalSignatureDto.toEtsLocalSignature(): EtsLocalSignature {
return EtsLocalSignature(
name = name,
Expand Down
2 changes: 2 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Signatures.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ data class FieldSignatureDto(
val declaringClass: ClassSignatureDto,
val name: String,
val type: TypeDto,
val wellKnownSymbol: String? = null,
)

@Serializable
Expand All @@ -53,6 +54,7 @@ data class MethodSignatureDto(
val name: String,
val parameters: List<MethodParameterDto>,
val returnType: TypeDto,
val wellKnownSymbol: String? = null,
)

@Serializable
Expand Down
2 changes: 2 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Signatures.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ data class EtsFieldSignature(
val enclosingClass: EtsClassSignature,
val name: String,
val type: EtsType,
val wellKnownSymbol: EtsWellKnownSymbol? = null,
) {
override fun toString(): String {
return "${enclosingClass.name}::$name: $type"
Expand All @@ -128,6 +129,7 @@ data class EtsMethodSignature(
val name: String,
val parameters: List<EtsMethodParameter>,
val returnType: EtsType,
val wellKnownSymbol: EtsWellKnownSymbol? = null,
) {
override fun toString(): String {
val params = parameters.joinToString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
import org.jacodb.ets.dto.AssignStmtDto
import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.dto.RawStmtDto
import org.jacodb.ets.dto.RawValueDto
import org.jacodb.ets.dto.toEtsFile
import org.jacodb.ets.model.EtsRawStmt
import org.jacodb.ets.model.EtsWellKnownSymbol
import org.jacodb.ets.utils.DEFAULT_ARK_CLASS_NAME
import org.jacodb.ets.utils.DEFAULT_ARK_METHOD_NAME
import org.jacodb.ets.utils.EtsIrProvider
Expand All @@ -48,11 +50,54 @@ class EtsTsPartialClassTest {
return EtsFileDto.loadFromJson(output.readText())
}

@Test
fun `constant computed members retain executable lifecycle and exact identities after model conversion`() {
val dto = frontend(
"""
export class Counter {
['value'] = 7;
[1e2] = 11;
[-0] = 13;
[Symbol.iterator]() { return 17; }
['Symbol.iterator']() { return 19; }
increment(value: number) { return value + this.value; }
}
export class Fields {
[Symbol.iterator] = 1;
['Symbol.iterator'] = 2;
}
export function construct() { return new Counter(); }
""".trimIndent(),
)

val model = dto.toEtsFile()
val counter = model.classes.single { it.name == "Counter" }
val symbolMethod = counter.methods.single { it.signature.wellKnownSymbol == EtsWellKnownSymbol.ITERATOR }
val stringMethod = counter.methods.single {
it.name == "Symbol.iterator" && it.signature.wellKnownSymbol == null
}
val fields = model.classes.single { it.name == "Fields" }.fields
val symbolField = fields.single { it.signature.wellKnownSymbol == EtsWellKnownSymbol.ITERATOR }
val stringField = fields.single { it.signature.wellKnownSymbol == null }
val module = model.classes.single { it.name == DEFAULT_ARK_CLASS_NAME }

assertEquals(expected = listOf("value", "100", "0"), actual = counter.fields.map { it.name })
assertNotEquals(illegal = stringMethod.signature, actual = symbolMethod.signature)
assertEquals(expected = "Symbol.iterator", actual = symbolMethod.name)
assertNotEquals(illegal = stringField.signature, actual = symbolField.signature)
for (method in counter.methods) {
assertFalse(method.cfg.stmts.any { it is EtsRawStmt }, message = "Unexpected unsupported body: ${method.name}")
}
assertTrue(dto.exportInfos.any { it.exportName == "Counter" })
assertFalse(module.methods.single { it.name == "construct" }.cfg.stmts.any { it is EtsRawStmt })
}

@Test
fun `ordinary bodies survive native frontend and model conversion with unsupported lifecycle`() {
val dto = frontend(
"""
class Base { [Symbol.iterator]() { return [][Symbol.iterator](); } }
const tag: unique symbol = Symbol("tag");
class Base { [tag]() { return []; } }
export class Counter extends Base {
own = 7;
increment(value: number) { return value + this.own; }
Expand Down
3 changes: 3 additions & 0 deletions jacodb-ets/ts-frontend/src/dto/signatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface FieldSignatureDto {
declaringClass: ClassSignatureDto;
name: string;
type: TypeDto;
/** Symbol keys remain distinct from string-named lookalikes. */
wellKnownSymbol?: "iterator";
}

export interface MethodParameterDto {
Expand All @@ -61,6 +63,7 @@ export interface MethodSignatureDto {
name: string;
parameters: MethodParameterDto[];
returnType: TypeDto;
wellKnownSymbol?: "iterator";
}

export interface LocalSignatureDto {
Expand Down
71 changes: 66 additions & 5 deletions jacodb-ets/ts-frontend/src/lowering/astUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,90 @@
/** Shared AST helpers used across the lowering modules. */

import * as ts from "typescript";
import { COMPUTED_MEMBER_NAME, DEFAULT_ARK_CLASS_NAME, Modifier, PATTERN_PARAMETER_PREFIX } from "../dto/constants";
import { COMPUTED_MEMBER_NAME, CONSTRUCTOR_NAME, DEFAULT_ARK_CLASS_NAME, INSTANCE_INIT_METHOD_NAME, Modifier, PATTERN_PARAMETER_PREFIX, STATIC_INIT_METHOD_NAME } from "../dto/constants";
import { DecoratorDto } from "../dto/model";
import { MethodParameterDto, MethodSignatureDto } from "../dto/signatures";
import { FunctionTypeDto, TypeDto, UNKNOWN_TYPE } from "../dto/types";
import type { LoweringContext } from "./methodBuilder";

export function memberName(name: ts.PropertyName | ts.BindingName | ts.EntityName): string {
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) || ts.isPrivateIdentifier(name)) {
const literal = ts.isComputedPropertyName(name) ? name.expression : name;
if (ts.isNumericLiteral(literal)) return String(Number(literal.text));
if (ts.isStringLiteral(literal) || ts.isNoSubstitutionTemplateLiteral(literal)) return literal.text;
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) {
return name.text;
}
return COMPUTED_MEMBER_NAME;
}

export interface ClassMemberKey {
name: string;
wellKnownSymbol?: "iterator";
}

/** Literal keys and the unshadowed standard-library iterator key. */
export function classMemberKey(name: ts.PropertyName, checker: ts.TypeChecker): ClassMemberKey | undefined {
if (!ts.isComputedPropertyName(name)) return { name: memberName(name) };

const expression = name.expression;
const isLiteral = ts.isStringLiteral(expression)
|| ts.isNoSubstitutionTemplateLiteral(expression)
|| ts.isNumericLiteral(expression);
if (isLiteral) return literalClassMemberKey(memberName(name));

const isNegativeNumber = ts.isPrefixUnaryExpression(expression)
&& expression.operator === ts.SyntaxKind.MinusToken
&& ts.isNumericLiteral(expression.operand);
if (isNegativeNumber) return literalClassMemberKey(String(-Number(expression.operand.text)));

const isIteratorAccess = ts.isPropertyAccessExpression(expression)
&& expression.name.text === "iterator"
&& ts.isIdentifier(expression.expression)
&& expression.expression.text === "Symbol";
if (!isIteratorAccess) return undefined;

const symbolDeclarations = resolvedSymbolOf(expression.expression, checker)?.declarations ?? [];
const iteratorDeclarations = resolvedSymbolOf(expression.name, checker)?.declarations ?? [];
const isLibraryDeclaration = (declaration: ts.Declaration): boolean => {
const source = declaration.getSourceFile();
return source.isDeclarationFile && source.hasNoDefaultLib
&& /(?:^|[/\\])lib\.es\d+(?:\.\w+)*\.d\.ts$/.test(source.fileName);
};
const isIntrinsic = symbolDeclarations.length > 0 && iteratorDeclarations.length > 0
&& symbolDeclarations.every(isLibraryDeclaration) && iteratorDeclarations.every(isLibraryDeclaration);
if (!isIntrinsic) return undefined;

return { name: "Symbol.iterator", wellKnownSymbol: "iterator" };
}

function literalClassMemberKey(name: string): ClassMemberKey | undefined {
const isLifecycleName = [CONSTRUCTOR_NAME, INSTANCE_INIT_METHOD_NAME, STATIC_INIT_METHOD_NAME].includes(name);
return isLifecycleName ? undefined : { name };
}

/** Definition semantics that prevent constructing a class value, independently of its retained method bodies. */
export function unsupportedClassDeclarationReason(decl: ts.ClassDeclaration, checker: ts.TypeChecker): string | undefined {
const visited = new Set<ts.ClassDeclaration>();
const reasonOf = (current: ts.ClassDeclaration): string | undefined => {
if (visited.has(current)) return undefined;
visited.add(current);

const hasComputedMember = current.members.some((member) =>
member.name !== undefined && ts.isComputedPropertyName(member.name));
if (hasComputedMember) return "computed class member needs definition-time key evaluation";
const hasUnsupportedComputedMember = current.members.some((member) => {
if (member.name === undefined || !ts.isComputedPropertyName(member.name)) return false;

const key = classMemberKey(member.name, checker);
if (key === undefined) return true;

const hasAccessorCollision = current.members.some((other) => {
if (!ts.isGetAccessorDeclaration(other) && !ts.isSetAccessorDeclaration(other)) return false;

const otherKey = classMemberKey(other.name, checker);
return otherKey?.name === key.name && otherKey?.wellKnownSymbol === key.wellKnownSymbol
&& (modifiersOf(other) & Modifier.STATIC) === (modifiersOf(member) & Modifier.STATIC);
});
return hasAccessorCollision;
});
if (hasUnsupportedComputedMember) return "computed class member needs definition-time key evaluation";

const extendsType = current.heritageClauses
?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)
Expand Down
48 changes: 38 additions & 10 deletions jacodb-ets/ts-frontend/src/lowering/classBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { BodyDto, ClassDto, FieldDto, MethodDto } from "../dto/model";
import { ClassSignatureDto, UNKNOWN_FILE_SIGNATURE } from "../dto/signatures";
import { ClassTypeDto, TypeDto, NUMBER_TYPE, STRING_TYPE, UNKNOWN_TYPE, VOID_TYPE } from "../dto/types";
import { buildParameters, decoratorsOf, memberName, modifiersOf, parameterType, returnTypeOf, unsupportedClassDeclarationReason } from "./astUtils";
import { buildParameters, classMemberKey, ClassMemberKey, decoratorsOf, memberName, modifiersOf, parameterType, returnTypeOf, unsupportedClassDeclarationReason } from "./astUtils";
import { unsupportedStmt } from "./diagnostics";
import { LoweringError, constant } from "./exprLowering";
import { LoweringContext, MethodContext } from "./methodBuilder";
Expand Down Expand Up @@ -69,17 +69,25 @@ export class ClassBuilder {
const staticInitializers: (ts.PropertyDeclaration | ts.ClassStaticBlockDeclaration)[] = [];
const fields: FieldDto[] = [];
const methods: MethodDto[] = [];
const computedMethods = new Set<MethodDto>();
let ctorDecl: ts.ConstructorDeclaration | undefined;

for (const member of decl.members) {
const hasComputedName = member.name !== undefined && ts.isComputedPropertyName(member.name);
if (hasComputedName) {
const key = member.name === undefined ? undefined : classMemberKey(member.name, this.ctx.checker);
const hasUnsupportedComputedName = member.name !== undefined && ts.isComputedPropertyName(member.name)
&& (key === undefined || key.name === CONSTRUCTOR_NAME);
if (hasUnsupportedComputedName) {
// Keep independent bodies without inventing storage for an unevaluated key.
continue;
}

if (ts.isPropertyDeclaration(member)) {
fields.push(this.buildField(signature, member));
const field = this.buildField(signature, member);
const previous = fields.findIndex((candidate) => candidate.signature.name === field.signature.name
&& candidate.signature.wellKnownSymbol === field.signature.wellKnownSymbol
&& (candidate.modifiers & Modifier.STATIC) === (field.modifiers & Modifier.STATIC));
if (previous >= 0) fields.splice(previous, 1);
fields.push(field);
if (member.initializer !== undefined) {
if (isStatic(member)) {
staticInitializers.push(member);
Expand All @@ -102,7 +110,22 @@ export class ClassBuilder {
ts.isGetAccessorDeclaration(member) ||
ts.isSetAccessorDeclaration(member)
) {
methods.push(this.buildMethodFromDecl(signature, member));
const method = this.buildMethodFromDecl(signature, member);
const sameKey = (candidate: MethodDto): boolean => candidate.signature.name === method.signature.name
&& candidate.signature.wellKnownSymbol === method.signature.wellKnownSymbol
&& (candidate.modifiers & Modifier.STATIC) === (method.modifiers & Modifier.STATIC);
const replacesComputedKey = ts.isComputedPropertyName(member.name)
|| methods.some((candidate) => computedMethods.has(candidate) && sameKey(candidate));
const replacesMethod = definitionFailure === undefined && replacesComputedKey
&& ts.isMethodDeclaration(member) && member.body !== undefined;
if (replacesMethod) {
// Runtime definitions replace earlier bodies; overload declarations are retained.
for (let index = methods.length - 1; index >= 0; index--) {
if (methods[index].body !== undefined && sameKey(methods[index])) methods.splice(index, 1);
}
}
if (ts.isComputedPropertyName(member.name)) computedMethods.add(method);
methods.push(method);
} else if (ts.isIndexSignatureDeclaration(member)) {
fields.push(this.buildIndexSignatureField(signature, member));
} else if (ts.isSemicolonClassElement(member)) {
Expand Down Expand Up @@ -344,13 +367,14 @@ export class ClassBuilder {
decl: ClassMemberDecl | ts.FunctionDeclaration,
nameOverride?: string,
): MethodDto {
const name = nameOverride ?? (decl.name !== undefined ? memberName(decl.name) : "");
const key = decl.name === undefined ? { name: "" } : this.memberKey(decl.name);
const name = nameOverride ?? key.name;
const { parameters, prologueParams } = buildParameters(this.ctx, decl);
const returnType = returnTypeOf(this.ctx, decl);

const method: MethodDto = {
isStrictMode: isStrictMode(decl),
signature: { declaringClass, name, parameters, returnType },
signature: { declaringClass, ...key, name, parameters, returnType },
modifiers: modifiersOf(decl),
decorators: decoratorsOf(decl),
};
Expand Down Expand Up @@ -639,7 +663,7 @@ export class ClassBuilder {
left: {
_: "InstanceFieldRef",
instance: thisLocal,
field: { declaringClass, name: memberName(field.name), type: fieldType },
field: { declaringClass, ...this.memberKey(field.name), type: fieldType },
},
right: lowerer.expr.lowerToImmediate(field.initializer!),
});
Expand Down Expand Up @@ -671,7 +695,7 @@ export class ClassBuilder {
_: "StaticFieldRef",
field: {
declaringClass,
name: memberName(initializer.name),
...this.memberKey(initializer.name),
type: this.fieldType(initializer),
},
},
Expand Down Expand Up @@ -700,7 +724,7 @@ export class ClassBuilder {
return {
signature: {
declaringClass,
name: memberName(decl.name),
...this.memberKey(decl.name),
type: this.fieldType(decl),
},
modifiers: modifiersOf(decl),
Expand All @@ -710,6 +734,10 @@ export class ClassBuilder {
};
}

private memberKey(name: ts.PropertyName): ClassMemberKey {
return classMemberKey(name, this.ctx.checker) ?? { name: memberName(name) };
}

private buildParameterPropertyField(
declaringClass: ClassSignatureDto,
p: ts.ParameterDeclaration,
Expand Down
Loading
Loading