Skip to content

Commit 6717de0

Browse files
authored
Fix variable initialization checks / revise flow logic (#2578)
BREAKING CHANGE: Initialization of global variables sometimes wasn't guaranteed before, allowing unsafe behavior if initialization indeed wasn't performed before access. To mitigate, variables from now on require either an initializer, a primitive type with a trivial default value (typically `0`), a nullable type (if a reference, defaulting to `null`) or otherwise annotation with definitive assignment (i.e. `let someObject!: ...`, then inserting a runtime check upon access).
1 parent 658b5e8 commit 6717de0

File tree

218 files changed

+71127
-66263
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

218 files changed

+71127
-66263
lines changed

src/compiler.ts

+407-458
Large diffs are not rendered by default.

src/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"Only variables, functions and enums become WebAssembly module exports.": 235,
5050
"Literal '{0}' does not fit into 'i64' or 'u64' types.": 236,
5151
"Index signature accessors in type '{0}' differ in types.": 237,
52+
"Initializer, definitive assignment or nullable type expected.": 238,
53+
"Definitive assignment has no effect on local variables.": 239,
5254

5355
"Importing the table disables some indirect call optimizations.": 901,
5456
"Exporting the table disables some indirect call optimizations.": 902,
@@ -106,6 +108,7 @@
106108
"Declaration expected.": 1146,
107109
"'const' declarations must be initialized.": 1155,
108110
"Unterminated regular expression literal.": 1161,
111+
"Declarations with initializers cannot also have definite assignment assertions.": 1263,
109112
"Interface declaration cannot have 'implements' clause.": 1176,
110113
"Binary digit expected.": 1177,
111114
"Octal digit expected.": 1178,
@@ -165,6 +168,7 @@
165168
"Variable '{0}' used before its declaration.": 2448,
166169
"Cannot redeclare block-scoped variable '{0}'" : 2451,
167170
"The type argument for type parameter '{0}' cannot be inferred from the usage. Consider specifying the type arguments explicitly.": 2453,
171+
"Variable '{0}' is used before being assigned.": 2454,
168172
"Type alias '{0}' circularly references itself.": 2456,
169173
"Type '{0}' has no property '{1}'.": 2460,
170174
"The '{0}' operator cannot be applied to type '{1}'.": 2469,

src/flow.ts

+89-41
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ export class Flow {
246246
thisFieldFlags: Map<Property,FieldFlags> | null = null;
247247
/** The label we break to when encountering a return statement, when inlining. */
248248
inlineReturnLabel: string | null = null;
249+
/** Alternative flows if a compound expression is true-ish. */
250+
trueFlows: Map<ExpressionRef,Flow> | null = null;
251+
/** Alternative flows if a compound expression is false-ish. */
252+
falseFlows: Map<ExpressionRef,Flow> | null = null;
249253

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

310314
/** Forks this flow to a child flow. */
311-
fork(resetBreakContext: bool = false): Flow {
315+
fork(
316+
/** Whether a new break context is established, e.g. by a block. */
317+
newBreakContext: bool = false,
318+
/** Whether a new continue context is established, e.g. by a loop. */
319+
newContinueContext: bool = newBreakContext
320+
): Flow {
312321
let branch = new Flow(this.targetFunction, this.inlineFunction);
313322
branch.parent = this;
323+
branch.flags = this.flags;
314324
branch.outer = this.outer;
315-
if (resetBreakContext) {
316-
branch.flags = this.flags & ~(
325+
if (newBreakContext) {
326+
branch.flags &= ~(
317327
FlowFlags.Breaks |
318-
FlowFlags.ConditionallyBreaks |
328+
FlowFlags.ConditionallyBreaks
329+
);
330+
} else {
331+
branch.breakLabel = this.breakLabel;
332+
}
333+
if (newContinueContext) {
334+
branch.flags &= ~(
319335
FlowFlags.Continues |
320336
FlowFlags.ConditionallyContinues
321337
);
322338
} else {
323-
branch.flags = this.flags;
324339
branch.continueLabel = this.continueLabel;
325-
branch.breakLabel = this.breakLabel;
326340
}
327341
branch.localFlags = this.localFlags.slice();
328342
if (this.sourceFunction.is(CommonFlags.Constructor)) {
@@ -335,6 +349,52 @@ export class Flow {
335349
return branch;
336350
}
337351

352+
/** Forks this flow to a child flow where `condExpr` is true-ish. */
353+
forkThen(
354+
/** Condition that turned out to be true. */
355+
condExpr: ExpressionRef,
356+
/** Whether a new break context is established, e.g. by a block. */
357+
newBreakContext: bool = false,
358+
/** Whether a new continue context is established, e.g. by a loop. */
359+
newContinueContext: bool = newBreakContext
360+
): Flow {
361+
let flow = this.fork(newBreakContext, newContinueContext);
362+
let trueFlows = this.trueFlows;
363+
if (trueFlows && trueFlows.has(condExpr)) {
364+
flow.inherit(changetype<Flow>(trueFlows.get(condExpr)));
365+
}
366+
flow.inheritNonnullIfTrue(condExpr);
367+
return flow;
368+
}
369+
370+
/** Remembers the alternative flow if `condExpr` turns out `true`. */
371+
noteThen(condExpr: ExpressionRef, trueFlow: Flow): void {
372+
let trueFlows = this.trueFlows;
373+
if (!trueFlows) this.trueFlows = trueFlows = new Map();
374+
trueFlows.set(condExpr, trueFlow);
375+
}
376+
377+
/** Forks this flow to a child flow where `condExpr` is false-ish. */
378+
forkElse(
379+
/** Condition that turned out to be false. */
380+
condExpr: ExpressionRef
381+
): Flow {
382+
let flow = this.fork();
383+
let falseFlows = this.falseFlows;
384+
if (falseFlows && falseFlows.has(condExpr)) {
385+
flow.inherit(changetype<Flow>(falseFlows.get(condExpr)));
386+
}
387+
flow.inheritNonnullIfFalse(condExpr);
388+
return flow;
389+
}
390+
391+
/** Remembers the alternative flow if `condExpr` turns out `false`. */
392+
noteElse(condExpr: ExpressionRef, falseFlow: Flow): void {
393+
let falseFlows = this.falseFlows;
394+
if (!falseFlows) this.falseFlows = falseFlows = new Map();
395+
falseFlows.set(condExpr, falseFlow);
396+
}
397+
338398
/** Gets a free temporary local of the specified type. */
339399
getTempLocal(type: Type): Local {
340400
let local = this.targetFunction.addLocal(type);
@@ -523,36 +583,27 @@ export class Flow {
523583
}
524584
}
525585

526-
/** Pushes a new break label to the stack, for example when entering a loop that one can `break` from. */
527-
pushBreakLabel(): string {
586+
/** Pushes a new control flow label, for example when entering a loop that one can `break` from. */
587+
pushControlFlowLabel(): i32 {
528588
let targetFunction = this.targetFunction;
529589
let id = targetFunction.nextBreakId++;
530590
let stack = targetFunction.breakStack;
531591
if (!stack) targetFunction.breakStack = [ id ];
532592
else stack.push(id);
533-
let label = id.toString();
534-
targetFunction.breakLabel = label;
535-
return label;
593+
return id;
536594
}
537595

538-
/** Pops the most recent break label from the stack. */
539-
popBreakLabel(): void {
596+
/** Pops the most recent control flow label and validates that it matches. */
597+
popControlFlowLabel(expectedLabel: i32): void {
540598
let targetFunction = this.targetFunction;
541-
let stack = assert(targetFunction.breakStack);
542-
let length = assert(stack.length);
543-
stack.pop();
544-
if (length > 1) {
545-
targetFunction.breakLabel = stack[length - 2].toString();
546-
} else {
547-
targetFunction.breakLabel = null;
548-
targetFunction.breakStack = null;
549-
}
599+
let stack = assert(targetFunction.breakStack); // should exist
600+
assert(stack.length); // should not be empty
601+
assert(stack.pop() == expectedLabel); // should match
550602
}
551603

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

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

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

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

587630
let thisFlags = this.flags;
588631
let otherFlags = other.flags;
@@ -653,8 +696,13 @@ export class Flow {
653696
}
654697

655698
this.flags = newFlags | (thisFlags & (FlowFlags.UncheckedContext | FlowFlags.CtorParamContext));
699+
}
656700

657-
// local flags
701+
/** Merges a branch joining again with this flow, i.e. then without else. */
702+
mergeBranch(other: Flow): void {
703+
this.mergeSideEffects(other);
704+
705+
// Local flags matter if the branch does not terminate
658706
let thisLocalFlags = this.localFlags;
659707
let numThisLocalFlags = thisLocalFlags.length;
660708
let otherLocalFlags = other.localFlags;
@@ -675,12 +723,12 @@ export class Flow {
675723
// only be set if it has been observed prior to entering the branch.
676724
}
677725

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

685733
let leftFlags = left.flags;
686734
let rightFlags = right.flags;
@@ -881,7 +929,7 @@ export class Flow {
881929
}
882930

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

9971045
/** Updates local states to reflect that this branch is only taken when `expr` is false-ish. */
998-
inheritNonnullIfFalse(
1046+
private inheritNonnullIfFalse(
9991047
/** Expression being false. */
10001048
expr: ExpressionRef,
10011049
/** If specified, only set the flag if also nonnull in this flow. */

src/module.ts

-26
Original file line numberDiff line numberDiff line change
@@ -3617,32 +3617,6 @@ export class BinaryModule {
36173617
) {}
36183618
}
36193619

3620-
/** Tests if an expression needs an explicit 'unreachable' when it is the terminating statement. */
3621-
export function needsExplicitUnreachable(expr: ExpressionRef): bool {
3622-
// not applicable if pushing a value to the stack
3623-
if (binaryen._BinaryenExpressionGetType(expr) != TypeRef.Unreachable) {
3624-
return false;
3625-
}
3626-
3627-
switch (binaryen._BinaryenExpressionGetId(expr)) {
3628-
case ExpressionId.Unreachable:
3629-
case ExpressionId.Return: return false;
3630-
case ExpressionId.Break: {
3631-
return binaryen._BinaryenBreakGetCondition(expr) != 0;
3632-
}
3633-
case ExpressionId.Block: {
3634-
if (!binaryen._BinaryenBlockGetName(expr)) { // can't break out of it
3635-
let numChildren = binaryen._BinaryenBlockGetNumChildren(expr); // last child needs unreachable
3636-
return (
3637-
numChildren > 0 &&
3638-
needsExplicitUnreachable(binaryen._BinaryenBlockGetChildAt(expr, numChildren - 1))
3639-
);
3640-
}
3641-
}
3642-
}
3643-
return true;
3644-
}
3645-
36463620
// TypeBuilder
36473621

36483622
const DEBUG_TYPEBUILDER = false;

src/parser.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,12 @@ export class Parser extends DiagnosticEmitter {
985985
}
986986
initializer = this.parseExpression(tn, Precedence.Comma + 1);
987987
if (!initializer) return null;
988+
if (flags & CommonFlags.DefinitelyAssigned) {
989+
this.error(
990+
DiagnosticCode.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions,
991+
initializer.range
992+
);
993+
}
988994
} else if (!isFor) {
989995
if (flags & CommonFlags.Const) {
990996
if (!(flags & CommonFlags.Ambient)) {
@@ -1001,7 +1007,7 @@ export class Parser extends DiagnosticEmitter {
10011007
}
10021008
}
10031009
let range = Range.join(identifier.range, tn.range());
1004-
if (initializer && (flags & CommonFlags.DefinitelyAssigned) != 0) {
1010+
if ((flags & CommonFlags.DefinitelyAssigned) != 0 && (flags & CommonFlags.Ambient) != 0) {
10051011
this.error(
10061012
DiagnosticCode.A_definite_assignment_assertion_is_not_permitted_in_this_context,
10071013
range
@@ -2383,12 +2389,15 @@ export class Parser extends DiagnosticEmitter {
23832389
}
23842390
initializer = this.parseExpression(tn);
23852391
if (!initializer) return null;
2392+
if (flags & CommonFlags.DefinitelyAssigned) {
2393+
this.error(
2394+
DiagnosticCode.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions,
2395+
name.range
2396+
);
2397+
}
23862398
}
23872399
let range = tn.range(startPos, tn.pos);
2388-
if (
2389-
(flags & CommonFlags.DefinitelyAssigned) != 0 &&
2390-
(isInterface || initializer || (flags & CommonFlags.Static) != 0)
2391-
) {
2400+
if ((flags & CommonFlags.DefinitelyAssigned) != 0 && (isInterface || (flags & CommonFlags.Ambient) != 0)) {
23922401
this.error(
23932402
DiagnosticCode.A_definite_assignment_assertion_is_not_permitted_in_this_context,
23942403
range

src/program.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ import {
138138
} from "./resolver";
139139

140140
import {
141-
Flow
141+
Flow,
142+
LocalFlags
142143
} from "./flow";
143144

144145
import {
@@ -1983,7 +1984,7 @@ export class Program extends DiagnosticEmitter {
19831984
}
19841985
}
19851986
classReference = classReference.base;
1986-
} while(classReference);
1987+
} while (classReference);
19871988
} else {
19881989
let signatureReference = type.getSignature();
19891990
if (signatureReference) {
@@ -3767,7 +3768,8 @@ export class Function extends TypedElement {
37673768
this.original = this;
37683769
let program = prototype.program;
37693770
this.type = signature.type;
3770-
this.flow = Flow.createDefault(this);
3771+
let flow = Flow.createDefault(this);
3772+
this.flow = flow;
37713773
if (!prototype.is(CommonFlags.Ambient)) {
37723774
let localIndex = 0;
37733775
let thisType = signature.thisType;
@@ -3782,6 +3784,7 @@ export class Function extends TypedElement {
37823784
if (!scopedLocals) this.flow.scopedLocals = scopedLocals = new Map();
37833785
scopedLocals.set(CommonNames.this_, local);
37843786
this.localsByIndex[local.index] = local;
3787+
flow.setLocalFlag(local.index, LocalFlags.Initialized);
37853788
}
37863789
let parameterTypes = signature.parameterTypes;
37873790
for (let i = 0, k = parameterTypes.length; i < k; ++i) {
@@ -3797,6 +3800,7 @@ export class Function extends TypedElement {
37973800
if (!scopedLocals) this.flow.scopedLocals = scopedLocals = new Map();
37983801
scopedLocals.set(parameterName, local);
37993802
this.localsByIndex[local.index] = local;
3803+
flow.setLocalFlag(local.index, LocalFlags.Initialized);
38003804
}
38013805
}
38023806
registerConcreteElement(program, this);
@@ -3872,15 +3876,13 @@ export class Function extends TypedElement {
38723876
// used by flows to keep track of break labels
38733877
nextBreakId: i32 = 0;
38743878
breakStack: i32[] | null = null;
3875-
breakLabel: string | null = null;
38763879

38773880
/** Finalizes the function once compiled, releasing no longer needed resources. */
38783881
finalize(module: Module, ref: FunctionRef): void {
38793882
this.ref = ref;
38803883
let breakStack = this.breakStack;
3881-
assert(!breakStack || !breakStack.length); // internal error
3882-
this.breakStack = breakStack = null;
3883-
this.breakLabel = null;
3884+
assert(!breakStack || !breakStack.length); // should be empty
3885+
this.breakStack = null;
38843886
this.addDebugInfo(module, ref);
38853887
}
38863888

std/assembly/rt/itcms.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { E_ALLOCATION_TOO_LARGE, E_ALREADY_PINNED, E_NOT_PINNED } from "../util/
4949
// @ts-ignore: decorator
5050
@lazy let pinSpace = initLazy(changetype<Object>(memory.data(offsetof<Object>())));
5151
// @ts-ignore: decorator
52-
@lazy let iter: Object; // null
52+
@lazy let iter: Object = changetype<Object>(0); // unsafe initializion below
5353

5454
function initLazy(space: Object): Object {
5555
space.nextWithColor = changetype<usize>(space);

std/assembly/rt/tlsf.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ import { E_ALLOCATION_TOO_LARGE } from "../util/error";
137137
@inline const ROOT_SIZE: usize = HL_END + sizeof<usize>();
138138

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

142142
/** Gets the second level map of the specified first level. */
143143
// @ts-ignore: decorator

0 commit comments

Comments
 (0)