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
6 changes: 6 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Convert.kt
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ import org.jacodb.ets.model.EtsUnknownType
import org.jacodb.ets.model.EtsUnsignedRightShiftExpr
import org.jacodb.ets.model.EtsValue
import org.jacodb.ets.model.EtsVoidType
import org.jacodb.ets.model.EtsWellKnownSymbol
import org.jacodb.ets.model.EtsYieldExpr

private val logger = KotlinLogging.logger {}
Expand Down Expand Up @@ -516,6 +517,11 @@ class EtsMethodBuilder(
callee = method.toEtsMethodSignature(),
args = args.map { ensureLocal(it.toEtsEntity()) },
type = type.toEtsType(),
wellKnownSymbol = when (wellKnownSymbol) {
null -> null
"iterator" -> EtsWellKnownSymbol.ITERATOR
else -> error("Unknown intrinsic symbol key: $wellKnownSymbol")
},
)

is StaticCallExprDto -> EtsStaticCallExpr(
Expand Down
1 change: 1 addition & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/dto/Values.kt
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ data class InstanceCallExprDto(
val instance: ValueDto, // Local
override val method: MethodSignatureDto,
override val args: List<ValueDto>,
val wellKnownSymbol: String? = null,
) : CallExprDto

@Serializable
Expand Down
4 changes: 4 additions & 0 deletions jacodb-ets/src/main/kotlin/org/jacodb/ets/model/Expr.kt
Original file line number Diff line number Diff line change
Expand Up @@ -851,11 +851,15 @@ interface EtsCallExpr : EtsExpr, CommonCallExpr {
override val args: List<EtsLocal>
}

/** Intrinsic symbol keys selected by language operations, independent of source bindings named Symbol. */
enum class EtsWellKnownSymbol { ITERATOR }

data class EtsInstanceCallExpr(
override val instance: EtsLocal,
override val callee: EtsMethodSignature,
override val args: List<EtsLocal>,
override val type: EtsType,
val wellKnownSymbol: EtsWellKnownSymbol? = null,
) : EtsCallExpr, CommonInstanceCallExpr {
override fun toString(): String {
return "call ${instance}.${callee.name}(${args.joinToString()})"
Expand Down
20 changes: 20 additions & 0 deletions jacodb-ets/src/test/kotlin/org/jacodb/ets/test/EtsTsSymbolTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ import org.jacodb.ets.utils.generateEtsIR
import org.junit.jupiter.api.Test
import org.jacodb.ets.dto.SymbolTypeDto
import org.jacodb.ets.model.EtsSymbolType
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsInstanceCallExpr
import org.jacodb.ets.model.EtsWellKnownSymbol

class EtsTsSymbolTest {
companion object {
Expand All @@ -54,6 +57,23 @@ class EtsTsSymbolTest {
}
}

@Test
fun `intrinsic iterator identity survives frontend JSON and model conversion`() {
val frontendDto = runFrontend(
source = "export function copy(input: number[], Symbol: any): number[] { return [...input]; }",
)
val roundTripped = EtsFileDto.loadFromJson(
Json { serializersModule = dtoModule }.encodeToString(frontendDto),
)
val method = roundTripped.toEtsFile().classes.single { it.name == DEFAULT_ARK_CLASS_NAME }
.methods.single { it.name == "copy" }
val calls = method.cfg.stmts.filterIsInstance<EtsAssignStmt>().mapNotNull { it.rhv as? EtsInstanceCallExpr }
val iteratorCall = calls.single { it.wellKnownSymbol == EtsWellKnownSymbol.ITERATOR }

assertEquals("Symbol.iterator", iteratorCall.callee.name)
assertTrue(iteratorCall.args.isEmpty())
}

@Test
fun `symbol parameter and return types survive frontend JSON and model conversion`() {
val frontendDto = runFrontend(source = "export function same(value: symbol): symbol { return value; }")
Expand Down
2 changes: 2 additions & 0 deletions jacodb-ets/ts-frontend/src/dto/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ export interface InstanceCallExprDto {
instance: LocalDto; // Kotlin Convert casts this to LocalDto — MUST be a Local
method: MethodSignatureDto;
args: ValueDto[];
/** Intrinsic key identity, not a property whose string name happens to mention Symbol. */
wellKnownSymbol?: "iterator";
}

export interface StaticCallExprDto {
Expand Down
26 changes: 26 additions & 0 deletions jacodb-ets/ts-frontend/src/lowering/iteratorLowering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class IteratorLowerer {
_: "InstanceCallExpr", instance: source,
method: { declaringClass: UNKNOWN_CLASS_SIGNATURE, name: "Symbol.iterator", parameters: [], returnType: UNKNOWN_TYPE },
args: [],
wellKnownSymbol: "iterator",
},
});
this.requireObject(this.iterator);
Expand Down Expand Up @@ -89,6 +90,31 @@ export class IteratorLowerer {
cfg.placeLabel(done);
}

/** A binding pattern that stops before exhaustion performs normal IteratorClose. */
close(): void {
const cfg = this.m.cfg;
const lookup = cfg.newLabel();
const invoke = cfg.newLabel();
const done = cfg.newLabel();
cfg.branch(this.isExhausted(), done, lookup);
cfg.placeLabel(lookup);
cfg.emit({ _: "AssignStmt", left: this.exhausted, right: bool(true) });
const method = this.field(this.iterator, "return", UNKNOWN_TYPE);
cfg.branch({ _: "ConditionExpr", op: "==", left: method, right: { _: "Constant", value: "null", type: { _: "NullType" } } }, done, invoke);
cfg.placeLabel(invoke);
const result = this.m.newTemp(UNKNOWN_TYPE);
cfg.emit({
_: "AssignStmt", left: result,
right: {
_: "PtrCallExpr", ptr: method, receiver: this.iterator,
method: { declaringClass: UNKNOWN_CLASS_SIGNATURE, name: "return", parameters: [], returnType: UNKNOWN_TYPE }, args: [],
},
});
this.requireObject(result);
cfg.goto(done);
cfg.placeLabel(done);
}

/** Abrupt binding initialization closes the iterator; the original throw wins. */
closeOnException(action: () => void): void {
const cfg = this.m.cfg;
Expand Down
69 changes: 29 additions & 40 deletions jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export class StmtLowerer {
instance: iterable,
method: unknownMethod("Symbol.iterator"),
args: [],
wellKnownSymbol: "iterator",
});

const headLabel = cfg.newLabel();
Expand Down Expand Up @@ -993,7 +994,7 @@ export class StmtLowerer {
}
}

/** `{a, b: {c}, d = 1}` / `[x, , y]` unpacked from `source` via field/array refs. */
/** Object bindings read properties; array bindings consume the iterator protocol. */
private lowerBindingPattern(pattern: ts.BindingPattern, source: LocalDto): void {
if (ts.isObjectBindingPattern(pattern)) {
const stableSource = this.expr.materialize({
Expand Down Expand Up @@ -1053,49 +1054,37 @@ export class StmtLowerer {
}
return;
}
// Array patterns with rest consume the iterator, including omitted elements.
if (pattern.elements.some((element) => ts.isBindingElement(element) && element.dotDotDotToken !== undefined)) {
if (pattern.elements.some((element) => ts.isBindingElement(element)
&& !ts.isIdentifier(element.name))) {
throw new LoweringError("nested bindings in array rest are not represented with iterator semantics");
}
const iterator = new IteratorLowerer(this.m, source);
for (const element of pattern.elements) {
if (ts.isOmittedExpression(element)) {
iterator.take(UNKNOWN_TYPE, { readValue: false });
continue;
}
if (element.dotDotDotToken === undefined) {
const value = iterator.take(this.bindingType(element.name));
iterator.closeOnException(() => this.bindDestructured(element.name, value, element.initializer));
continue;
}
const type = this.bindingType(element.name);
const elementType = type._ === "ArrayType" ? arrayElementType(type) : UNKNOWN_TYPE;
const rest = this.m.newTemp(type._ === "ArrayType" ? type : { _: "ArrayType", elementType, dimensions: 1 });
this.m.cfg.emit({ _: "AssignStmt", left: rest, right: { _: "NewArrayExpr", elementType, size: constant("0", NUMBER_TYPE) } });
const offset = this.m.newTemp(NUMBER_TYPE);
this.m.cfg.emit({ _: "AssignStmt", left: offset, right: constant("0", NUMBER_TYPE) });
iterator.appendTo(rest, offset, elementType);
this.bindDestructured(element.name, rest, undefined);
}
return;
// Every array binding consumes the iterator, including patterns without rest and omitted elements.
const hasRest = pattern.elements.some((element) => ts.isBindingElement(element)
&& element.dotDotDotToken !== undefined);
const hasNestedBinding = pattern.elements.some((element) => ts.isBindingElement(element)
&& !ts.isIdentifier(element.name));
if (hasRest && hasNestedBinding) {
throw new LoweringError("nested bindings in array rest are not represented with iterator semantics");
}
pattern.elements.forEach((element, index) => {

const iterator = new IteratorLowerer(this.m, source);
for (const element of pattern.elements) {
if (ts.isOmittedExpression(element)) {
return;
iterator.take(UNKNOWN_TYPE, { readValue: false });
continue;
}
if (element.dotDotDotToken !== undefined) {
throw new LoweringError("rest element in array destructuring");
if (element.dotDotDotToken === undefined) {
const value = iterator.take(this.bindingType(element.name));
iterator.closeOnException(() => this.bindDestructured(element.name, value, element.initializer));
continue;
}
const ref: ValueDto = {
_: "ArrayRef",
array: source,
index: constant(String(index), NUMBER_TYPE),
type: this.bindingType(element.name),
};
this.bindDestructured(element.name, ref, element.initializer);
});

const type = this.bindingType(element.name);
const elementType = type._ === "ArrayType" ? arrayElementType(type) : UNKNOWN_TYPE;
const rest = this.m.newTemp(type._ === "ArrayType" ? type : { _: "ArrayType", elementType, dimensions: 1 });
this.m.cfg.emit({ _: "AssignStmt", left: rest, right: { _: "NewArrayExpr", elementType, size: constant("0", NUMBER_TYPE) } });
const offset = this.m.newTemp(NUMBER_TYPE);
this.m.cfg.emit({ _: "AssignStmt", left: offset, right: constant("0", NUMBER_TYPE) });
iterator.appendTo(rest, offset, elementType);
this.bindDestructured(element.name, rest, undefined);
}
iterator.close();
}

private bindingType(name: ts.BindingName): TypeDto {
Expand Down
3 changes: 3 additions & 0 deletions jacodb-ets/ts-frontend/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,9 @@ function validateValue(
if (value.instance._ !== "Local") {
err(`${ctx}: InstanceCallExpr.instance has kind '${value.instance._}', must be Local`);
}
if (value.wellKnownSymbol !== undefined && value.wellKnownSymbol !== "iterator") {
err(`${ctx}: unknown intrinsic instance-call symbol`);
}
break;
case "PropertyRef":
case "InstanceFieldRef":
Expand Down
Loading
Loading