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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jacodb.ets.dto.EtsFileDto
import org.jacodb.ets.dto.dtoModule
import org.jacodb.ets.dto.toEtsFile
import org.jacodb.ets.model.EtsAssignStmt
import org.jacodb.ets.model.EtsIfStmt
import org.jacodb.ets.model.EtsNewArrayExpr
import org.jacodb.ets.model.EtsPtrCallExpr
import org.jacodb.ets.utils.EtsIrProvider
Expand Down Expand Up @@ -63,4 +64,13 @@ class EtsTsArrayParameterTest {
assertTrue(values.any { it is EtsNewArrayExpr })
assertTrue(values.filterIsInstance<EtsPtrCallExpr>().any { it.callee.name == "next" && it.receiver != null })
}

@Test
fun `parameter defaults survive frontend JSON and model conversion`() {
val dto = roundTrip("export function value(a = 3, b = a + 1): number { return b; }")

val method = dto.toEtsFile().classes.flatMap { it.methods }.single { it.name == "value" }

assertTrue(method.cfg.stmts.any { it is EtsIfStmt })
}
}
9 changes: 8 additions & 1 deletion jacodb-ets/ts-frontend/src/lowering/astUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,13 @@ export function parameterType(ctx: LoweringContext, p: ts.ParameterDeclaration):

export interface BuiltParameters {
parameters: MethodParameterDto[];
prologueParams: { name: string; type: TypeDto; identifier?: ts.Identifier; pattern?: ts.BindingPattern }[];
prologueParams: {
name: string;
type: TypeDto;
identifier?: ts.Identifier;
pattern?: ts.BindingPattern;
initializer?: ts.Expression;
}[];
}

export function buildParameters(ctx: LoweringContext, decl: ts.SignatureDeclarationBase): BuiltParameters {
Expand All @@ -316,6 +322,7 @@ export function buildParameters(ctx: LoweringContext, decl: ts.SignatureDeclarat
type,
identifier: ts.isIdentifier(p.name) ? p.name : undefined,
pattern: ts.isObjectBindingPattern(p.name) || ts.isArrayBindingPattern(p.name) ? p.name : undefined,
initializer: p.initializer,
});
}
return { parameters, prologueParams };
Expand Down
25 changes: 6 additions & 19 deletions jacodb-ets/ts-frontend/src/lowering/classBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,10 @@ export class ClassBuilder {
if (decl.body !== undefined) {
const isStaticMethod = (modifiersOf(decl) & Modifier.STATIC) !== 0;
const m = new MethodContext(this.ctx, declaringClass, name, isStaticMethod);
m.emitPrologue(prologueParams);
const lowerer = new StmtLowerer(m);
this.lowerParameterPatterns(lowerer, m, prologueParams);
m.emitPrologue(prologueParams, (parameter, local) =>
lowerer.lowerParameterBinding(parameter, local),
);
lowerer.lowerStatements(decl.body.statements);
method.body = m.build();
}
Expand Down Expand Up @@ -343,9 +344,10 @@ export class ClassBuilder {
const classType: ClassTypeDto = { _: "ClassType", signature: declaringClass };

const m = new MethodContext(this.ctx, declaringClass, CONSTRUCTOR_NAME);
m.emitPrologue(prologueParams);
const lowerer = new StmtLowerer(m);
this.lowerParameterPatterns(lowerer, m, prologueParams);
m.emitPrologue(prologueParams, (parameter, local) =>
lowerer.lowerParameterBinding(parameter, local),
);
const thisLocal = m.getOrCreateLocal("this", classType);
const emitInitializers = (): void => {
// Parameter properties are initialized at constructor entry (after
Expand Down Expand Up @@ -420,21 +422,6 @@ export class ClassBuilder {
};
}

private lowerParameterPatterns(
lowerer: StmtLowerer,
m: MethodContext,
parameters: { name: string; type: TypeDto; pattern?: ts.BindingPattern }[],
): void {
for (const parameter of parameters) {
if (parameter.pattern !== undefined) {
lowerer.lowerParameterBindingPattern(
parameter.pattern,
m.getOrCreateLocal(parameter.name, parameter.type),
);
}
}
}

private emitParameterProperties(
m: MethodContext,
thisLocal: ReturnType<MethodContext["getOrCreateLocal"]>,
Expand Down
50 changes: 39 additions & 11 deletions jacodb-ets/ts-frontend/src/lowering/exprLowering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ import {
} from "../dto/values";
import { syntaxKindName, unsupportedValue } from "./diagnostics";
import { MethodContext } from "./methodBuilder";
import { IteratorLowerer } from "./iteratorLowering";
import { IteratorLowerer, throwRuntimeError } from "./iteratorLowering";

const RELATION_BY_SYNTAX: Partial<Record<ts.SyntaxKind, RelationOp>> = {
[ts.SyntaxKind.EqualsEqualsToken]: "==",
Expand Down Expand Up @@ -126,7 +126,10 @@ export class LoweringError extends Error {}
export type FunctionBodyLowerer = (
m: MethodContext,
body: ts.ConciseBody,
parameters?: BuiltParameters["prologueParams"],
emitPrologue: (afterParameter: (
parameter: BuiltParameters["prologueParams"][number],
local: LocalDto,
) => void) => void,
) => void;

type OptionalChainSegment = ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression;
Expand Down Expand Up @@ -325,6 +328,10 @@ export class ExprLowerer {
// ------------------------------------------------------------------

private lowerIdentifier(node: ts.Identifier): ValueDto {
if (this.m.isUninitializedParameter(node)) {
throwRuntimeError(this.m, "ReferenceError", `Cannot access '${node.text}' before initialization`);
return constant("undefined", UNDEFINED_TYPE);
}
const nonFiniteNumber = this.builtInNonFiniteNumber(node);
if (nonFiniteNumber !== undefined) {
return constant(nonFiniteNumber, NUMBER_TYPE);
Expand Down Expand Up @@ -637,6 +644,9 @@ export class ExprLowerer {

/** Assignment target. */
lowerLValue(node: ts.Expression, laterExpression?: ts.Expression): LValueDto {
if (ts.isIdentifier(node) && this.m.isUninitializedParameter(node)) {
throwRuntimeError(this.m, "ReferenceError", `Cannot access '${node.text}' before initialization`);
}
if (ts.isParenthesizedExpression(node)) {
return this.lowerLValue(node.expression, laterExpression);
}
Expand Down Expand Up @@ -829,6 +839,15 @@ export class ExprLowerer {
/** `x = e`, `x += e`, obj.f = e, arr[i] = e; returns the assigned value. */
lowerAssignment(node: ts.BinaryExpression): ValueDto {
const opKind = node.operatorToken.kind;
const targetNode = unwrapTransparentExpression(node.left);
if (opKind === ts.SyntaxKind.EqualsToken
&& ts.isIdentifier(targetNode) && this.m.isUninitializedParameter(targetNode)) {
// Resolving the binding is allowed; PutValue checks the TDZ after the RHS.
const rhs = this.lowerToImmediate(node.right);
throwRuntimeError(this.m, "ReferenceError", `Cannot access '${targetNode.text}' before initialization`);
return rhs;
}

const target = this.lowerLValue(node.left, node.right);

if (
Expand Down Expand Up @@ -1616,7 +1635,11 @@ export class ExprLowerer {
const { parameters, prologueParams } = buildParameters(this.m.ctx, node);
const returnType = returnTypeOf(this.m.ctx, node);
const baseSignature: MethodSignatureDto = { declaringClass, name, parameters, returnType };
const captures = collectCapturedIdentifiers(node, this.m.checker)
const capturedIdentifiers = collectCapturedIdentifiers(node, this.m.checker);
if (capturedIdentifiers.some((identifier) => this.m.isUninitializedParameter(identifier))) {
throw new LoweringError("closure capturing an uninitialized parameter requires a runtime temporal-dead-zone cell");
}
const captures = capturedIdentifiers
.filter((identifier) => this.m.moduleFieldForIdentifier(identifier) === undefined)
.map((identifier) => this.m.captureForIdentifier(identifier, this.safeTypeOf(identifier)));

Expand Down Expand Up @@ -1661,12 +1684,15 @@ export class ExprLowerer {
name,
ts.isArrowFunction(node) && this.m.isStaticMethod,
);
if (environment === undefined) {
closureContext.emitPrologue(prologueParams);
} else {
closureContext.emitClosurePrologue(environment.name, environment.type, captures, prologueParams, lexicalThis);
}
this.lowerFunctionBody(closureContext, node.body, prologueParams);
this.lowerFunctionBody(closureContext, node.body, (afterParameter) => {
if (environment === undefined) {
closureContext.emitPrologue(prologueParams, afterParameter);
} else {
closureContext.emitClosurePrologue(
environment.name, environment.type, captures, prologueParams, afterParameter, lexicalThis,
);
}
});
registry.methods.push({
signature,
modifiers: modifiersOf(node),
Expand Down Expand Up @@ -1753,9 +1779,10 @@ export class ExprLowerer {
returnType: returnTypeOf(this.m.ctx, property),
};
const methodContext = new MethodContext(this.m.ctx, signature, methodName);
methodContext.emitPrologue(prologueParams);
if (property.body !== undefined) {
this.lowerFunctionBody(methodContext, property.body, prologueParams);
this.lowerFunctionBody(methodContext, property.body, (afterParameter) =>
methodContext.emitPrologue(prologueParams, afterParameter),
);
}
methods.push({
signature: methodSignature,
Expand Down Expand Up @@ -2229,6 +2256,7 @@ function collectCapturedIdentifiers(
}
ts.forEachChild(node, visit);
};
for (const parameter of closure.parameters) visit(parameter);
visit(closure.body);
// Declaration nodes may be visited after their first reference, so filter at the end.
for (const symbol of [...captures.keys()]) {
Expand Down
75 changes: 61 additions & 14 deletions jacodb-ets/ts-frontend/src/lowering/methodBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ClassSignatureDto, FieldSignatureDto, FileSignatureDto } from "../dto/s
import { ClassTypeDto, LexicalEnvTypeDto, TypeDto, UNKNOWN_TYPE } from "../dto/types";
import { ClosureFieldRefDto, LocalDto, StaticFieldRefDto, ValueDto } from "../dto/values";
import { TypeConverter } from "../types/convert";
import type { BuiltParameters } from "./astUtils";
import { CfgBuilder } from "./cfg";
import { Diagnostics, syntaxKindName } from "./diagnostics";

Expand Down Expand Up @@ -72,6 +73,7 @@ export class MethodContext {
private readonly localNameCounters = new Map<string, number>();
private tempCount = 0;
private closureEnvCount = 0;
private readonly uninitializedParameters = new Set<ts.Symbol>();

constructor(
readonly ctx: LoweringContext,
Expand Down Expand Up @@ -289,8 +291,42 @@ export class MethodContext {
return this.getOrCreateLocal(name, type);
}

/** Parameter bindings are initially in the temporal dead zone, including patterns. */
private beginParameterInitialization(parameters: BuiltParameters["prologueParams"]): void {
const addBinding = (name: ts.BindingName): void => {
if (ts.isIdentifier(name)) {
const symbol = this.symbolForIdentifier(name);
if (symbol !== undefined) this.uninitializedParameters.add(symbol);
} else {
for (const element of name.elements) {
if (ts.isBindingElement(element)) addBinding(element.name);
}
}
};
for (const parameter of parameters) {
if (parameter.identifier !== undefined) addBinding(parameter.identifier);
if (parameter.pattern !== undefined) addBinding(parameter.pattern);
}
}

isUninitializedParameter(identifier: ts.Identifier): boolean {
const symbol = this.symbolForIdentifier(identifier);
return symbol !== undefined && this.uninitializedParameters.has(symbol);
}

markParameterInitialized(identifier: ts.Identifier): void {
const symbol = this.symbolForIdentifier(identifier);
if (symbol !== undefined) this.uninitializedParameters.delete(symbol);
}

/** Emit the standard prologue: parameter assignments, then `this := ThisRef`. */
emitPrologue(parameters: { name: string; type: TypeDto; identifier?: ts.Identifier }[]): void {
emitPrologue(
parameters: BuiltParameters["prologueParams"],
afterParameter?: (parameter: BuiltParameters["prologueParams"][number], local: LocalDto) => void,
): void {
this.beginParameterInitialization(parameters);
const earlyThis = parameters.some((parameter) => parameter.initializer !== undefined || parameter.pattern !== undefined);
if (earlyThis) this.emitThisAssignment();
parameters.forEach((param, index) => {
const local = param.identifier !== undefined
? this.localForIdentifier(param.identifier, param.type)
Expand All @@ -300,8 +336,11 @@ export class MethodContext {
left: local,
right: { _: "ParameterRef", index, type: param.type },
});
afterParameter?.(param, local);
if (param.identifier !== undefined) this.markParameterInitialized(param.identifier);
});
this.emitThisAssignment();
this.uninitializedParameters.clear();
if (!earlyThis) this.emitThisAssignment();
}

/**
Expand All @@ -312,7 +351,8 @@ export class MethodContext {
environmentName: string,
environmentType: LexicalEnvTypeDto,
captures: ClosureCapture[],
parameters: { name: string; type: TypeDto; identifier?: ts.Identifier }[],
parameters: BuiltParameters["prologueParams"],
afterParameter?: (parameter: BuiltParameters["prologueParams"][number], local: LocalDto) => void,
lexicalThis?: LocalDto,
): void {
const environment = this.getOrCreateLocal(environmentName, environmentType);
Expand All @@ -321,16 +361,6 @@ export class MethodContext {
left: environment,
right: { _: "ParameterRef", index: 0, type: environmentType },
});
parameters.forEach((param, index) => {
const local = param.identifier !== undefined
? this.localForIdentifier(param.identifier, param.type)
: this.getOrCreateLocal(param.name, param.type);
this.cfg.emit({
_: "AssignStmt",
left: local,
right: { _: "ParameterRef", index: index + 1, type: param.type },
});
});
const forwardedEnvironments = new Map<string, LocalDto>();
for (const capture of captures) {
const symbol = this.symbolForIdentifier(capture.identifier);
Expand Down Expand Up @@ -373,7 +403,24 @@ export class MethodContext {
fieldName: lexicalThis.name,
type: lexicalThis.type,
};
this.emitThisAssignment(receiver);

this.beginParameterInitialization(parameters);
const earlyThis = parameters.some((parameter) => parameter.initializer !== undefined || parameter.pattern !== undefined);
if (earlyThis) this.emitThisAssignment(receiver);
parameters.forEach((param, index) => {
const local = param.identifier !== undefined
? this.localForIdentifier(param.identifier, param.type)
: this.getOrCreateLocal(param.name, param.type);
this.cfg.emit({
_: "AssignStmt",
left: local,
right: { _: "ParameterRef", index: index + 1, type: param.type },
});
afterParameter?.(param, local);
if (param.identifier !== undefined) this.markParameterInitialized(param.identifier);
});
this.uninitializedParameters.clear();
if (!earlyThis) this.emitThisAssignment(receiver);
}

private emitThisAssignment(receiver?: ClosureFieldRefDto): void {
Expand Down
25 changes: 13 additions & 12 deletions jacodb-ets/ts-frontend/src/lowering/stmtLowering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MethodSignatureDto, UNKNOWN_CLASS_SIGNATURE, UNKNOWN_FILE_SIGNATURE } f
import { BOOLEAN_TYPE, NUMBER_TYPE, STRING_TYPE, TypeDto, UNDEFINED_TYPE, UNKNOWN_TYPE } from "../dto/types";
import { ImmediateDto, LValueDto, LocalDto, ValueDto } from "../dto/values";
import { exportAssignmentSupport } from "./astUtils";
import type { BuiltParameters } from "./astUtils";
import { Label } from "./cfg";
import { IteratorLowerer } from "./iteratorLowering";
import { unsupportedStmt } from "./diagnostics";
Expand Down Expand Up @@ -70,16 +71,9 @@ export class StmtLowerer {
) {
// Nested function bodies (closures, object-literal methods) are lowered
// with a fresh StmtLowerer over their own MethodContext.
this.expr = new ExprLowerer(m, (nestedContext, body, parameters) => {
this.expr = new ExprLowerer(m, (nestedContext, body, emitPrologue) => {
const nested = new StmtLowerer(nestedContext);
parameters?.forEach((parameter) => {
if (parameter.pattern !== undefined) {
nested.lowerParameterBindingPattern(
parameter.pattern,
nestedContext.getOrCreateLocal(parameter.name, parameter.type),
);
}
});
emitPrologue((parameter, local) => nested.lowerParameterBinding(parameter, local));
if (ts.isBlock(body)) {
nested.lowerStatements(body.statements);
} else {
Expand Down Expand Up @@ -827,9 +821,15 @@ export class StmtLowerer {
// Destructuring
// ------------------------------------------------------------------

/** Unpack one pattern parameter after its `%patN := ParameterRef(i)` prologue binding. */
lowerParameterBindingPattern(pattern: ts.BindingPattern, source: LocalDto): void {
this.lowerBindingPattern(pattern, source);
/** Apply a parameter default and unpack its pattern before the next argument is bound. */
lowerParameterBinding(parameter: BuiltParameters["prologueParams"][number], source: LocalDto): void {
const initializer = parameter.initializer;
if (initializer !== undefined) {
this.m.withOrigin(initializer, () => this.emitDefaultValue(source, parameter.type, initializer));
}
if (parameter.pattern !== undefined) {
this.lowerBindingPattern(parameter.pattern, source);
}
}

/** `{a, b: {c}, d = 1}` / `[x, , y]` unpacked from `source` via field/array refs. */
Expand Down Expand Up @@ -950,6 +950,7 @@ export class StmtLowerer {
if (defaultInit !== undefined) {
this.emitDefaultValue(destination, type, defaultInit);
}
this.m.markParameterInitialized(target);
return;
}
// Nested pattern: unpack through a temp.
Expand Down
Loading