6
*/
7
8
import {CompilerError} from '../CompilerError';
9
+import {Environment} from '../HIR';
10
import {
11
areEqualPaths,
12
BlockId,
23
PrunedReactiveScopeBlock,
24
ReactiveFunction,
25
ReactiveInstruction,
26
+ ReactiveOptionalCallValue,
27
ReactiveScope,
28
ReactiveScopeBlock,
29
ReactiveScopeDependency,
67
});
68
}
69
}
68
- visitReactiveFunction(
69
- fn,
70
- new PropagationVisitor(fn.env.config.enableTreatFunctionDepsAsConditional),
71
- context,
72
- );
70
+ visitReactiveFunction(fn, new PropagationVisitor(fn.env), context);
71
}
72
73
type TemporariesUsedOutsideDefiningScope = {
463
#getProperty(
464
object: Place,
465
property: string,
466
+ optional: boolean,
467
): ReactiveScopePropertyDependency {
468
const resolvedObject = this.resolveTemporary(object);
469
const resolvedDependency = this.#properties.get(resolvedObject.identifier);
484
};
485
}
486
488
- objectDependency.path.push({property, optional: false});
487
+ objectDependency.path.push({property, optional});
488
489
return objectDependency;
490
}
491
493
- declareProperty(lvalue: Place, object: Place, property: string): void {
494
- const nextDependency = this.#getProperty(object, property);
492
+ declareProperty(
493
+ lvalue: Place,
494
+ object: Place,
495
+ property: string,
496
+ optional: boolean,
497
+ ): void {
498
+ const nextDependency = this.#getProperty(object, property, optional);
499
this.#properties.set(lvalue.identifier, nextDependency);
500
}
501
575
this.visitDependency(dependency);
576
}
577
574
- visitProperty(object: Place, property: string): void {
575
- const nextDependency = this.#getProperty(object, property);
578
+ visitProperty(object: Place, property: string, optional: boolean): void {
579
+ const nextDependency = this.#getProperty(object, property, optional);
580
this.visitDependency(nextDependency);
581
}
582
675
}
676
677
class PropagationVisitor extends ReactiveFunctionVisitor<Context> {
674
- enableTreatFunctionDepsAsConditional = false;
678
+ env: Environment;
679
676
- constructor(enableTreatFunctionDepsAsConditional: boolean) {
680
+ constructor(env: Environment) {
681
super();
678
- this.enableTreatFunctionDepsAsConditional =
679
- enableTreatFunctionDepsAsConditional;
682
+ this.env = env;
683
}
684
685
override visitScope(scope: ReactiveScopeBlock, context: Context): void {
747
});
748
}
749
750
+ extractOptionalProperty(
751
+ context: Context,
752
+ optionalValue: ReactiveOptionalCallValue,
753
+ lvalue: Place,
754
+ ): {
755
+ lvalue: Place;
756
+ object: Place;
757
+ property: string;
758
+ optional: boolean;
759
+ } | null {
760
+ const sequence = optionalValue.value;
761
+ CompilerError.invariant(sequence.kind === 'SequenceExpression', {
762
+ reason: 'Expected OptionalExpression value to be a SequenceExpression',
763
+ description: `Found a \`${sequence.kind}\``,
764
+ loc: sequence.loc,
765
+ });
766
+ /**
767
+ * Base case: inner `<variable> "." or "?."" <property>`
768
+ *```
769
+ * <lvalue> = OptionalExpression optional=true (`optionalValue` is here)
770
+ * Sequence (`sequence` is here)
771
+ * t0 = LoadLocal <variable>
772
+ * Sequence
773
+ * t1 = PropertyLoad t0 . <property>
774
+ * LoadLocal t1
775
+ * ```
776
+ */
777
+ if (
778
+ sequence.instructions.length === 1 &&
779
+ sequence.instructions[0].value.kind === 'LoadLocal' &&
780
+ sequence.instructions[0].lvalue !== null &&
781
+ sequence.instructions[0].value.place.identifier.name !== null &&
782
+ !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) &&
783
+ sequence.value.kind === 'SequenceExpression' &&
784
+ sequence.value.instructions.length === 1 &&
785
+ sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
786
+ sequence.value.instructions[0].value.object.identifier.id ===
787
+ sequence.instructions[0].lvalue.identifier.id &&
788
+ sequence.value.instructions[0].lvalue !== null &&
789
+ sequence.value.value.kind === 'LoadLocal' &&
790
+ sequence.value.value.place.identifier.id ===
791
+ sequence.value.instructions[0].lvalue.identifier.id
792
+ ) {
793
+ context.declareTemporary(
794
+ sequence.instructions[0].lvalue,
795
+ sequence.instructions[0].value.place,
796
+ );
797
+ const propertyLoad = sequence.value.instructions[0].value;
798
+ return {
799
+ lvalue,
800
+ object: propertyLoad.object,
801
+ property: propertyLoad.property,
802
+ optional: optionalValue.optional,
803
+ };
804
+ }
805
+ /**
806
+ * Composed case: `<base-case> "." or "?." <property>`
807
+ *
808
+ * This case is convoluted, note how `t0` appears as an lvalue *twice*
809
+ * and then is an operand of an intermediate LoadLocal and then the
810
+ * object of the final PropertyLoad:
811
+ *
812
+ * ```
813
+ * <lvalue> = OptionalExpression optional=false (`optionalValue` is here)
814
+ * Sequence (`sequence` is here)
815
+ * t0 = Sequence
816
+ * t0 =
817
+ * <nested>
818
+ * LoadLocal t0
819
+ * Sequence
820
+ * t1 = PropertyLoad t0. <property>
821
+ * LoadLocal t1
822
+ * ```
823
+ */
824
+ if (
825
+ sequence.instructions.length === 1 &&
826
+ sequence.instructions[0].value.kind === 'SequenceExpression' &&
827
+ sequence.instructions[0].value.instructions.length === 1 &&
828
+ sequence.instructions[0].value.instructions[0].lvalue !== null &&
829
+ sequence.instructions[0].value.instructions[0].value.kind ===
830
+ 'OptionalExpression' &&
831
+ sequence.instructions[0].value.value.kind === 'LoadLocal' &&
832
+ sequence.instructions[0].value.value.place.identifier.id ===
833
+ sequence.instructions[0].value.instructions[0].lvalue.identifier.id &&
834
+ sequence.value.kind === 'SequenceExpression' &&
835
+ sequence.value.instructions.length === 1 &&
836
+ sequence.value.instructions[0].lvalue !== null &&
837
+ sequence.value.instructions[0].value.kind === 'PropertyLoad' &&
838
+ sequence.value.instructions[0].value.object.identifier.id ===
839
+ sequence.instructions[0].value.value.place.identifier.id &&
840
+ sequence.value.value.kind === 'LoadLocal' &&
841
+ sequence.value.value.place.identifier.id ===
842
+ sequence.value.instructions[0].lvalue.identifier.id
843
+ ) {
844
+ const {lvalue: innerLvalue, value: innerOptional} =
845
+ sequence.instructions[0].value.instructions[0];
846
+ const innerProperty = this.extractOptionalProperty(
847
+ context,
848
+ innerOptional,
849
+ innerLvalue,
850
+ );
851
+ if (innerProperty === null) {
852
+ return null;
853
+ }
854
+ context.declareProperty(
855
+ innerProperty.lvalue,
856
+ innerProperty.object,
857
+ innerProperty.property,
858
+ innerProperty.optional,
859
+ );
860
+ const propertyLoad = sequence.value.instructions[0].value;
861
+ return {
862
+ lvalue,
863
+ object: propertyLoad.object,
864
+ property: propertyLoad.property,
865
+ optional: optionalValue.optional,
866
+ };
867
+ }
868
+ return null;
869
+ }
870
+
871
+ visitOptionalExpression(
872
+ context: Context,
873
+ id: InstructionId,
874
+ value: ReactiveOptionalCallValue,
875
+ lvalue: Place | null,
876
+ ): void {
877
+ /**
878
+ * If this is the first optional=true optional in a recursive OptionalExpression
879
+ * subtree, we check to see if the subtree is of the form:
880
+ * ```
881
+ * NestedOptional =
882
+ * `<variable> . / ?. <property>`
883
+ * `<nested-optional> . / ?. <property>`
884
+ * ```
885
+ *
886
+ * Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains
887
+ * any other types of expressions - for example `foo?.[makeKey(a)]` - then this
888
+ * will return null and we'll go to the default handling below.
889
+ *
890
+ * If the tree does match the NestedOptional shape, then we'll have recorded
891
+ * a sequence of declareProperty calls, and the final visitProperty call here
892
+ * will record that optional chain as a dependency (since we know it's about
893
+ * to be referenced via its lvalue which is non-null).
894
+ */
895
+ if (
896
+ lvalue !== null &&
897
+ value.optional &&
898
+ this.env.config.enableOptionalDependencies
899
+ ) {
900
+ const inner = this.extractOptionalProperty(context, value, lvalue);
901
+ if (inner !== null) {
902
+ context.visitProperty(inner.object, inner.property, inner.optional);
903
+ return;
904
+ }
905
+ }
906
+
907
+ // Otherwise we treat everything after the optional as conditional
908
+ const inner = value.value;
909
+ /*
910
+ * OptionalExpression value is a SequenceExpression where the instructions
911
+ * represent the code prior to the `?` and the final value represents the
912
+ * conditional code that follows.
913
+ */
914
+ CompilerError.invariant(inner.kind === 'SequenceExpression', {
915
+ reason: 'Expected OptionalExpression value to be a SequenceExpression',
916
+ description: `Found a \`${value.kind}\``,
917
+ loc: value.loc,
918
+ suggestions: null,
919
+ });
920
+ // Instructions are the unconditionally executed portion before the `?`
921
+ for (const instr of inner.instructions) {
922
+ this.visitInstruction(instr, context);
923
+ }
924
+ // The final value is the conditional portion following the `?`
925
+ context.enterConditional(() => {
926
+ this.visitReactiveValue(context, id, inner.value, null);
927
+ });
928
+ }
929
+
930
visitReactiveValue(
931
context: Context,
932
id: InstructionId,
933
value: ReactiveValue,
934
+ lvalue: Place | null,
935
): void {
936
switch (value.kind) {
937
case 'OptionalExpression': {
754
- const inner = value.value;
755
- /*
756
- * OptionalExpression value is a SequenceExpression where the instructions
757
- * represent the code prior to the `?` and the final value represents the
758
- * conditional code that follows.
759
- */
760
- CompilerError.invariant(inner.kind === 'SequenceExpression', {
761
- reason:
762
- 'Expected OptionalExpression value to be a SequenceExpression',
763
- description: `Found a \`${value.kind}\``,
764
- loc: value.loc,
765
- suggestions: null,
766
- });
767
- // Instructions are the unconditionally executed portion before the `?`
768
- for (const instr of inner.instructions) {
769
- this.visitInstruction(instr, context);
770
- }
771
- // The final value is the conditional portion following the `?`
772
- context.enterConditional(() => {
773
- this.visitReactiveValue(context, id, inner.value);
774
- });
938
+ this.visitOptionalExpression(context, id, value, lvalue);
939
break;
940
}
941
case 'LogicalExpression': {
778
- this.visitReactiveValue(context, id, value.left);
942
+ this.visitReactiveValue(context, id, value.left, null);
943
context.enterConditional(() => {
780
- this.visitReactiveValue(context, id, value.right);
944
+ this.visitReactiveValue(context, id, value.right, null);
945
});
946
break;
947
}
948
case 'ConditionalExpression': {
785
- this.visitReactiveValue(context, id, value.test);
949
+ this.visitReactiveValue(context, id, value.test, null);
950
951
const consequentDeps = context.enterConditional(() => {
788
- this.visitReactiveValue(context, id, value.consequent);
952
+ this.visitReactiveValue(context, id, value.consequent, null);
953
});
954
const alternateDeps = context.enterConditional(() => {
791
- this.visitReactiveValue(context, id, value.alternate);
955
+ this.visitReactiveValue(context, id, value.alternate, null);
956
});
957
context.promoteDepsFromExhaustiveConditionals([
958
consequentDeps,
968
break;
969
}
970
case 'FunctionExpression': {
807
- if (this.enableTreatFunctionDepsAsConditional) {
971
+ if (this.env.config.enableTreatFunctionDepsAsConditional) {
972
context.enterConditional(() => {
973
for (const operand of eachInstructionValueOperand(value)) {
974
context.visitOperand(operand);
1015
}
1016
} else if (value.kind === 'PropertyLoad') {
1017
if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) {
854
- context.declareProperty(lvalue, value.object, value.property);
1018
+ context.declareProperty(lvalue, value.object, value.property, false);
1019
} else {
856
- context.visitProperty(value.object, value.property);
1020
+ context.visitProperty(value.object, value.property, false);
1021
}
1022
} else if (value.kind === 'StoreLocal') {
1023
context.visitOperand(value.value);
1060
});
1061
}
1062
} else {
899
- this.visitReactiveValue(context, id, value);
1063
+ this.visitReactiveValue(context, id, value, lvalue);
1064
}
1065
}
1066
1111
break;
1112
}
1113
case 'for': {
950
- this.visitReactiveValue(context, terminal.id, terminal.init);
951
- this.visitReactiveValue(context, terminal.id, terminal.test);
1114
+ this.visitReactiveValue(context, terminal.id, terminal.init, null);
1115
+ this.visitReactiveValue(context, terminal.id, terminal.test, null);
1116
context.enterConditional(() => {
1117
this.visitBlock(terminal.loop, context);
1118
if (terminal.update !== null) {
955
- this.visitReactiveValue(context, terminal.id, terminal.update);
1119
+ this.visitReactiveValue(
1120
+ context,
1121
+ terminal.id,
1122
+ terminal.update,
1123
+ null,
1124
+ );
1125
}
1126
});
1127
break;
1128
}
1129
case 'for-of': {
961
- this.visitReactiveValue(context, terminal.id, terminal.init);
1130
+ this.visitReactiveValue(context, terminal.id, terminal.init, null);
1131
context.enterConditional(() => {
1132
this.visitBlock(terminal.loop, context);
1133
});
1134
break;
1135
}
1136
case 'for-in': {
968
- this.visitReactiveValue(context, terminal.id, terminal.init);
1137
+ this.visitReactiveValue(context, terminal.id, terminal.init, null);
1138
context.enterConditional(() => {
1139
this.visitBlock(terminal.loop, context);
1140
});
1143
case 'do-while': {
1144
this.visitBlock(terminal.loop, context);
1145
context.enterConditional(() => {
977
- this.visitReactiveValue(context, terminal.id, terminal.test);
1146
+ this.visitReactiveValue(context, terminal.id, terminal.test, null);
1147
});
1148
break;
1149
}
1150
case 'while': {
982
- this.visitReactiveValue(context, terminal.id, terminal.test);
1151
+ this.visitReactiveValue(context, terminal.id, terminal.test, null);
1152
context.enterConditional(() => {
1153
this.visitBlock(terminal.loop, context);
1154
});