Skip to content

Fix variable initialization checks / revise flow logic #2578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Dec 5, 2022
Merged
865 changes: 407 additions & 458 deletions src/compiler.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"Only variables, functions and enums become WebAssembly module exports.": 235,
"Literal '{0}' does not fit into 'i64' or 'u64' types.": 236,
"Index signature accessors in type '{0}' differ in types.": 237,
"Initializer, definitive assignment or nullable type expected.": 238,
"Definitive assignment has no effect on local variables.": 239,

"Importing the table disables some indirect call optimizations.": 901,
"Exporting the table disables some indirect call optimizations.": 902,
Expand Down Expand Up @@ -106,6 +108,7 @@
"Declaration expected.": 1146,
"'const' declarations must be initialized.": 1155,
"Unterminated regular expression literal.": 1161,
"Declarations with initializers cannot also have definite assignment assertions.": 1263,
"Interface declaration cannot have 'implements' clause.": 1176,
"Binary digit expected.": 1177,
"Octal digit expected.": 1178,
Expand Down Expand Up @@ -165,6 +168,7 @@
"Variable '{0}' used before its declaration.": 2448,
"Cannot redeclare block-scoped variable '{0}'" : 2451,
"The type argument for type parameter '{0}' cannot be inferred from the usage. Consider specifying the type arguments explicitly.": 2453,
"Variable '{0}' is used before being assigned.": 2454,
"Type alias '{0}' circularly references itself.": 2456,
"Type '{0}' has no property '{1}'.": 2460,
"The '{0}' operator cannot be applied to type '{1}'.": 2469,
Expand Down
130 changes: 89 additions & 41 deletions src/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ export class Flow {
thisFieldFlags: Map<Property,FieldFlags> | null = null;
/** The label we break to when encountering a return statement, when inlining. */
inlineReturnLabel: string | null = null;
/** Alternative flows if a compound expression is true-ish. */
trueFlows: Map<ExpressionRef,Flow> | null = null;
/** Alternative flows if a compound expression is false-ish. */
falseFlows: Map<ExpressionRef,Flow> | null = null;

/** Tests if this is an inline flow. */
get isInline(): bool {
Expand Down Expand Up @@ -308,21 +312,31 @@ export class Flow {
}

/** Forks this flow to a child flow. */
fork(resetBreakContext: bool = false): Flow {
fork(
/** Whether a new break context is established, e.g. by a block. */
newBreakContext: bool = false,
/** Whether a new continue context is established, e.g. by a loop. */
newContinueContext: bool = newBreakContext
): Flow {
let branch = new Flow(this.targetFunction, this.inlineFunction);
branch.parent = this;
branch.flags = this.flags;
branch.outer = this.outer;
if (resetBreakContext) {
branch.flags = this.flags & ~(
if (newBreakContext) {
branch.flags &= ~(
FlowFlags.Breaks |
FlowFlags.ConditionallyBreaks |
FlowFlags.ConditionallyBreaks
);
} else {
branch.breakLabel = this.breakLabel;
}
if (newContinueContext) {
branch.flags &= ~(
FlowFlags.Continues |
FlowFlags.ConditionallyContinues
);
} else {
branch.flags = this.flags;
branch.continueLabel = this.continueLabel;
branch.breakLabel = this.breakLabel;
}
branch.localFlags = this.localFlags.slice();
if (this.sourceFunction.is(CommonFlags.Constructor)) {
Expand All @@ -335,6 +349,52 @@ export class Flow {
return branch;
}

/** Forks this flow to a child flow where `condExpr` is true-ish. */
forkThen(
/** Condition that turned out to be true. */
condExpr: ExpressionRef,
/** Whether a new break context is established, e.g. by a block. */
newBreakContext: bool = false,
/** Whether a new continue context is established, e.g. by a loop. */
newContinueContext: bool = newBreakContext
): Flow {
let flow = this.fork(newBreakContext, newContinueContext);
let trueFlows = this.trueFlows;
if (trueFlows && trueFlows.has(condExpr)) {
flow.inherit(changetype<Flow>(trueFlows.get(condExpr)));
}
flow.inheritNonnullIfTrue(condExpr);
return flow;
}

/** Remembers the alternative flow if `condExpr` turns out `true`. */
noteThen(condExpr: ExpressionRef, trueFlow: Flow): void {
let trueFlows = this.trueFlows;
if (!trueFlows) this.trueFlows = trueFlows = new Map();
trueFlows.set(condExpr, trueFlow);
}

/** Forks this flow to a child flow where `condExpr` is false-ish. */
forkElse(
/** Condition that turned out to be false. */
condExpr: ExpressionRef
): Flow {
let flow = this.fork();
let falseFlows = this.falseFlows;
if (falseFlows && falseFlows.has(condExpr)) {
flow.inherit(changetype<Flow>(falseFlows.get(condExpr)));
}
flow.inheritNonnullIfFalse(condExpr);
return flow;
}

/** Remembers the alternative flow if `condExpr` turns out `false`. */
noteElse(condExpr: ExpressionRef, falseFlow: Flow): void {
let falseFlows = this.falseFlows;
if (!falseFlows) this.falseFlows = falseFlows = new Map();
falseFlows.set(condExpr, falseFlow);
}

/** Gets a free temporary local of the specified type. */
getTempLocal(type: Type): Local {
let local = this.targetFunction.addLocal(type);
Expand Down Expand Up @@ -523,36 +583,27 @@ export class Flow {
}
}

/** Pushes a new break label to the stack, for example when entering a loop that one can `break` from. */
pushBreakLabel(): string {
/** Pushes a new control flow label, for example when entering a loop that one can `break` from. */
pushControlFlowLabel(): i32 {
let targetFunction = this.targetFunction;
let id = targetFunction.nextBreakId++;
let stack = targetFunction.breakStack;
if (!stack) targetFunction.breakStack = [ id ];
else stack.push(id);
let label = id.toString();
targetFunction.breakLabel = label;
return label;
return id;
}

/** Pops the most recent break label from the stack. */
popBreakLabel(): void {
/** Pops the most recent control flow label and validates that it matches. */
popControlFlowLabel(expectedLabel: i32): void {
let targetFunction = this.targetFunction;
let stack = assert(targetFunction.breakStack);
let length = assert(stack.length);
stack.pop();
if (length > 1) {
targetFunction.breakLabel = stack[length - 2].toString();
} else {
targetFunction.breakLabel = null;
targetFunction.breakStack = null;
}
let stack = assert(targetFunction.breakStack); // should exist
assert(stack.length); // should not be empty
assert(stack.pop() == expectedLabel); // should match
}

/** Inherits flags of another flow into this one, i.e. a finished inner block. */
inherit(other: Flow): void {
assert(other.targetFunction == this.targetFunction);
assert(other.parent == this); // currently the case, but might change
let otherFlags = other.flags;

// respective inner flags are irrelevant if contexts differ
Expand All @@ -571,18 +622,10 @@ export class Flow {
this.thisFieldFlags = other.thisFieldFlags;
}

/** Inherits flags of a conditional branch joining again with this one, i.e. then without else. */
inheritBranch(other: Flow, conditionKind: ConditionKind = ConditionKind.Unknown): void {
assert(other.targetFunction == this.targetFunction);
switch (conditionKind) {
case ConditionKind.True: this.inherit(other); // always executes
case ConditionKind.False: return; // never executes
}

// Note that flags in `this` flow have already happened. For instance,
// a return cannot be undone no matter what'd happen in subsequent branches,
// but an allocation, which doesn't terminate, can become conditional. Not
// all flags have a corresponding conditional flag that's tracked.
/** Merges only the side effects of a branch, i.e. when not taken. */
mergeSideEffects(other: Flow): void {
assert(other.targetFunction == this.targetFunction);

let thisFlags = this.flags;
let otherFlags = other.flags;
Expand Down Expand Up @@ -653,8 +696,13 @@ export class Flow {
}

this.flags = newFlags | (thisFlags & (FlowFlags.UncheckedContext | FlowFlags.CtorParamContext));
}

// local flags
/** Merges a branch joining again with this flow, i.e. then without else. */
mergeBranch(other: Flow): void {
this.mergeSideEffects(other);

// Local flags matter if the branch does not terminate
let thisLocalFlags = this.localFlags;
let numThisLocalFlags = thisLocalFlags.length;
let otherLocalFlags = other.localFlags;
Expand All @@ -675,12 +723,12 @@ export class Flow {
// only be set if it has been observed prior to entering the branch.
}

/** Inherits mutual flags of two alternate branches becoming this one, i.e. then with else. */
inheritMutual(left: Flow, right: Flow): void {
/** Inherits two alternate branches to become this flow, i.e. then with else. */
inheritAlternatives(left: Flow, right: Flow): void {
assert(left.targetFunction == right.targetFunction);
assert(left.targetFunction == this.targetFunction);
// This differs from the previous method in that no flags are guaranteed
// to happen unless it is the case in both flows.
// Differs from `mergeBranch` in that the alternatives are intersected to
// then become this branch.

let leftFlags = left.flags;
let rightFlags = right.flags;
Expand Down Expand Up @@ -881,7 +929,7 @@ export class Flow {
}

/** Updates local states to reflect that this branch is only taken when `expr` is true-ish. */
inheritNonnullIfTrue(
private inheritNonnullIfTrue(
/** Expression being true. */
expr: ExpressionRef,
/** If specified, only set the flag if also nonnull in this flow. */
Expand Down Expand Up @@ -995,7 +1043,7 @@ export class Flow {
}

/** Updates local states to reflect that this branch is only taken when `expr` is false-ish. */
inheritNonnullIfFalse(
private inheritNonnullIfFalse(
/** Expression being false. */
expr: ExpressionRef,
/** If specified, only set the flag if also nonnull in this flow. */
Expand Down
26 changes: 0 additions & 26 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3617,32 +3617,6 @@ export class BinaryModule {
) {}
}

/** Tests if an expression needs an explicit 'unreachable' when it is the terminating statement. */
export function needsExplicitUnreachable(expr: ExpressionRef): bool {
// not applicable if pushing a value to the stack
if (binaryen._BinaryenExpressionGetType(expr) != TypeRef.Unreachable) {
return false;
}

switch (binaryen._BinaryenExpressionGetId(expr)) {
case ExpressionId.Unreachable:
case ExpressionId.Return: return false;
case ExpressionId.Break: {
return binaryen._BinaryenBreakGetCondition(expr) != 0;
}
case ExpressionId.Block: {
if (!binaryen._BinaryenBlockGetName(expr)) { // can't break out of it
let numChildren = binaryen._BinaryenBlockGetNumChildren(expr); // last child needs unreachable
return (
numChildren > 0 &&
needsExplicitUnreachable(binaryen._BinaryenBlockGetChildAt(expr, numChildren - 1))
);
}
}
}
return true;
}

// TypeBuilder

const DEBUG_TYPEBUILDER = false;
Expand Down
19 changes: 14 additions & 5 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,12 @@ export class Parser extends DiagnosticEmitter {
}
initializer = this.parseExpression(tn, Precedence.Comma + 1);
if (!initializer) return null;
if (flags & CommonFlags.DefinitelyAssigned) {
this.error(
DiagnosticCode.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions,
initializer.range
);
}
} else if (!isFor) {
if (flags & CommonFlags.Const) {
if (!(flags & CommonFlags.Ambient)) {
Expand All @@ -1001,7 +1007,7 @@ export class Parser extends DiagnosticEmitter {
}
}
let range = Range.join(identifier.range, tn.range());
if (initializer && (flags & CommonFlags.DefinitelyAssigned) != 0) {
if ((flags & CommonFlags.DefinitelyAssigned) != 0 && (flags & CommonFlags.Ambient) != 0) {
this.error(
DiagnosticCode.A_definite_assignment_assertion_is_not_permitted_in_this_context,
range
Expand Down Expand Up @@ -2383,12 +2389,15 @@ export class Parser extends DiagnosticEmitter {
}
initializer = this.parseExpression(tn);
if (!initializer) return null;
if (flags & CommonFlags.DefinitelyAssigned) {
this.error(
DiagnosticCode.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions,
name.range
);
}
}
let range = tn.range(startPos, tn.pos);
if (
(flags & CommonFlags.DefinitelyAssigned) != 0 &&
(isInterface || initializer || (flags & CommonFlags.Static) != 0)
) {
if ((flags & CommonFlags.DefinitelyAssigned) != 0 && (isInterface || (flags & CommonFlags.Ambient) != 0)) {
this.error(
DiagnosticCode.A_definite_assignment_assertion_is_not_permitted_in_this_context,
range
Expand Down
16 changes: 9 additions & 7 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ import {
} from "./resolver";

import {
Flow
Flow,
LocalFlags
} from "./flow";

import {
Expand Down Expand Up @@ -1983,7 +1984,7 @@ export class Program extends DiagnosticEmitter {
}
}
classReference = classReference.base;
} while(classReference);
} while (classReference);
} else {
let signatureReference = type.getSignature();
if (signatureReference) {
Expand Down Expand Up @@ -3767,7 +3768,8 @@ export class Function extends TypedElement {
this.original = this;
let program = prototype.program;
this.type = signature.type;
this.flow = Flow.createDefault(this);
let flow = Flow.createDefault(this);
this.flow = flow;
if (!prototype.is(CommonFlags.Ambient)) {
let localIndex = 0;
let thisType = signature.thisType;
Expand All @@ -3782,6 +3784,7 @@ export class Function extends TypedElement {
if (!scopedLocals) this.flow.scopedLocals = scopedLocals = new Map();
scopedLocals.set(CommonNames.this_, local);
this.localsByIndex[local.index] = local;
flow.setLocalFlag(local.index, LocalFlags.Initialized);
}
let parameterTypes = signature.parameterTypes;
for (let i = 0, k = parameterTypes.length; i < k; ++i) {
Expand All @@ -3797,6 +3800,7 @@ export class Function extends TypedElement {
if (!scopedLocals) this.flow.scopedLocals = scopedLocals = new Map();
scopedLocals.set(parameterName, local);
this.localsByIndex[local.index] = local;
flow.setLocalFlag(local.index, LocalFlags.Initialized);
}
}
registerConcreteElement(program, this);
Expand Down Expand Up @@ -3872,15 +3876,13 @@ export class Function extends TypedElement {
// used by flows to keep track of break labels
nextBreakId: i32 = 0;
breakStack: i32[] | null = null;
breakLabel: string | null = null;

/** Finalizes the function once compiled, releasing no longer needed resources. */
finalize(module: Module, ref: FunctionRef): void {
this.ref = ref;
let breakStack = this.breakStack;
assert(!breakStack || !breakStack.length); // internal error
this.breakStack = breakStack = null;
this.breakLabel = null;
assert(!breakStack || !breakStack.length); // should be empty
this.breakStack = null;
this.addDebugInfo(module, ref);
}

Expand Down
2 changes: 1 addition & 1 deletion std/assembly/rt/itcms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { E_ALLOCATION_TOO_LARGE, E_ALREADY_PINNED, E_NOT_PINNED } from "../util/
// @ts-ignore: decorator
@lazy let pinSpace = initLazy(changetype<Object>(memory.data(offsetof<Object>())));
// @ts-ignore: decorator
@lazy let iter: Object; // null
@lazy let iter: Object = changetype<Object>(0); // unsafe initializion below

function initLazy(space: Object): Object {
space.nextWithColor = changetype<usize>(space);
Expand Down
2 changes: 1 addition & 1 deletion std/assembly/rt/tlsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ import { E_ALLOCATION_TOO_LARGE } from "../util/error";
@inline const ROOT_SIZE: usize = HL_END + sizeof<usize>();

// @ts-ignore: decorator
@lazy export let ROOT: Root;
@lazy export let ROOT: Root = changetype<Root>(0); // unsafe initializion below

/** Gets the second level map of the specified first level. */
// @ts-ignore: decorator
Expand Down
Loading