Skip to content

Commit 3ffeb11

Browse files
committed
refactor(compiler): template type-checking support for '@boundary'
1 parent 6e4b947 commit 3ffeb11

15 files changed

Lines changed: 414 additions & 16 deletions

File tree

packages/compiler/src/render3/r3_boundaries.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ export function createBoundaryBlock(
7777
continue;
7878
}
7979

80+
const aliasMatch = param.expression.match(
81+
/^([$A-Z_][0-9A-Z_$]*)\s*=\s*([$A-Z_][0-9A-Z_$]*)$/i,
82+
);
83+
if (aliasMatch) {
84+
const name = aliasMatch[1];
85+
const variableName = aliasMatch[2];
86+
87+
if (variableName !== '$error' && variableName !== '$retry') {
88+
errors.push(
89+
new ParseError(
90+
param.sourceSpan,
91+
`Unknown context variable "${variableName}". Only "$error" and "$retry" are allowed`,
92+
),
93+
);
94+
} else if (contextVariables.some((v) => v.name === name)) {
95+
errors.push(new ParseError(param.sourceSpan, `Duplicate parameter variable "${name}"`));
96+
} else {
97+
const varSpan = param.sourceSpan;
98+
contextVariables.push(new t.Variable(name, variableName, varSpan, varSpan));
99+
}
100+
continue;
101+
}
102+
80103
const whenMatch = param.expression.match(WHEN_PATTERN);
81104
if (whenMatch) {
82105
if (expression !== null) {

packages/compiler/src/template/pipeline/ir/src/enums.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export enum OpKind {
8989
*/
9090
BoundaryCreate,
9191

92+
/**
93+
* Create a boundary error branch creation instruction op.
94+
*/
95+
BoundaryErrorCreate,
96+
9297
/**
9398
* An op to validate and handle errors in a template.
9499
*/

packages/compiler/src/template/pipeline/ir/src/expression.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,7 @@ export function transformExpressionsInOp(
13441344
case OpKind.Control:
13451345
case OpKind.ControlCreate:
13461346
case OpKind.BoundaryCreate:
1347+
case OpKind.BoundaryErrorCreate:
13471348
// These operations contain no expressions.
13481349
break;
13491350
default:
@@ -1438,6 +1439,10 @@ export function transformExpressionsInExpression(
14381439
expr.expr = transformExpressionsInExpression(expr.expr, transform, flags);
14391440
} else if (expr instanceof o.SpreadElementExpr) {
14401441
expr.expression = transformExpressionsInExpression(expr.expression, transform, flags);
1442+
} else if (expr instanceof o.FunctionExpr) {
1443+
for (let i = 0; i < expr.statements.length; i++) {
1444+
transformExpressionsInStatement(expr.statements[i], transform, flags);
1445+
}
14411446
} else if (
14421447
expr instanceof o.ReadVarExpr ||
14431448
expr instanceof o.ExternalExpr ||

packages/compiler/src/template/pipeline/ir/src/ops/create.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {SecurityContext} from '../../../../../core';
1010
import * as i18n from '../../../../../i18n/i18n_ast';
1111
import * as o from '../../../../../output/output_ast';
1212
import {ParseSourceSpan} from '../../../../../parse_util';
13+
import * as t from '../../../../../render3/r3_ast';
1314
import {
1415
AnimationKind,
1516
BindingKind,
@@ -80,7 +81,8 @@ export type CreateOp =
8081
| AnimationOp
8182
| SourceLocationOp
8283
| ControlCreateOp
83-
| BoundaryCreateOp;
84+
| BoundaryCreateOp
85+
| BoundaryErrorCreateOp;
8486

8587
/**
8688
* An operation representing the creation of an element or container.
@@ -504,6 +506,67 @@ export function createBoundaryCreateOp(
504506
};
505507
}
506508

509+
/**
510+
* An op that creates a boundary error block.
511+
*/
512+
export interface BoundaryErrorCreateOp extends Op<CreateOp>, ConsumesSlotOpTrait {
513+
kind: OpKind.BoundaryErrorCreate;
514+
515+
templateKind: TemplateKind;
516+
517+
decls: number | null;
518+
519+
vars: number | null;
520+
521+
functionNameSuffix: string;
522+
523+
i18nPlaceholder?: i18n.TagPlaceholder | i18n.BlockPlaceholder;
524+
525+
/**
526+
* The Xref of the BoundaryCreate op that this error branch belongs to.
527+
*/
528+
boundaryXref: XrefId;
529+
530+
contextVariables: t.Variable[];
531+
532+
/**
533+
* The handle to the slot allocated for this element.
534+
*/
535+
handle: SlotHandle;
536+
537+
startSourceSpan: ParseSourceSpan;
538+
539+
wholeSourceSpan: ParseSourceSpan;
540+
}
541+
542+
export function createBoundaryErrorCreateOp(
543+
xref: XrefId,
544+
templateKind: TemplateKind,
545+
functionNameSuffix: string,
546+
i18nPlaceholder: i18n.TagPlaceholder | i18n.BlockPlaceholder | undefined,
547+
startSourceSpan: ParseSourceSpan,
548+
wholeSourceSpan: ParseSourceSpan,
549+
boundaryXref: XrefId,
550+
contextVariables: t.Variable[],
551+
): BoundaryErrorCreateOp {
552+
return {
553+
kind: OpKind.BoundaryErrorCreate,
554+
xref,
555+
templateKind,
556+
handle: new SlotHandle(),
557+
functionNameSuffix,
558+
decls: null,
559+
vars: null,
560+
i18nPlaceholder,
561+
startSourceSpan,
562+
wholeSourceSpan,
563+
boundaryXref,
564+
contextVariables,
565+
...TRAIT_CONSUMES_SLOT,
566+
...NEW_OP,
567+
};
568+
}
569+
507570
/**
508571
* An op that creates a repeater (e.g. a for loop).
509572
*/

packages/compiler/src/template/pipeline/ir/src/ops/update.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,11 @@ export interface BoundaryOp extends Op<UpdateOp>, DependsOnSlotContextOpTrait, C
736736
*/
737737
primaryTarget: XrefId;
738738

739+
/**
740+
* The primary branch (guarded by the no-error condition).
741+
*/
742+
guarded: ConditionalCaseExpr;
743+
739744
/**
740745
* Each possible error fallback view that could be displayed.
741746
*/
@@ -761,13 +766,15 @@ export interface BoundaryOp extends Op<UpdateOp>, DependsOnSlotContextOpTrait, C
761766
export function createBoundaryOp(
762767
target: XrefId,
763768
primaryTarget: XrefId,
769+
guarded: ConditionalCaseExpr,
764770
conditions: Array<ConditionalCaseExpr>,
765771
sourceSpan: ParseSourceSpan,
766772
): BoundaryOp {
767773
return {
768774
kind: OpKind.Boundary,
769775
target,
770776
primaryTarget,
777+
guarded,
771778
conditions,
772779
processed: null,
773780
sourceSpan,

packages/compiler/src/template/pipeline/src/ingest.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -595,15 +595,15 @@ function ingestBoundaryBlock(unit: ViewCompilationUnit, boundaryBlock: t.Boundar
595595
const tagName = ingestControlFlowInsertionPoint(unit, errorView.xref, errorBlock);
596596

597597
// Create branch creation operation
598-
const branchCreateOp = ir.createConditionalBranchCreateOp(
598+
const branchCreateOp = ir.createBoundaryErrorCreateOp(
599599
errorView.xref,
600600
ir.TemplateKind.Block,
601-
tagName,
602601
'Error',
603-
ir.Namespace.HTML,
604602
undefined,
605603
errorBlock.startSourceSpan,
606604
errorBlock.sourceSpan,
605+
createOp.xref,
606+
errorBlock.contextVariables,
607607
);
608608
unit.create.push(branchCreateOp);
609609

@@ -612,11 +612,13 @@ function ingestBoundaryBlock(unit: ViewCompilationUnit, boundaryBlock: t.Boundar
612612
? convertAst(errorBlock.expression, unit.job, null)
613613
: null;
614614

615+
const errorVar = errorBlock.contextVariables.find((v) => v.value === '$error');
616+
615617
const conditionalCaseExpr = new ir.ConditionalCaseExpr(
616618
caseExpr,
617619
branchCreateOp.xref,
618620
branchCreateOp.handle,
619-
null,
621+
errorVar || null,
620622
);
621623
conditions.push(conditionalCaseExpr);
622624

@@ -637,11 +639,17 @@ function ingestBoundaryBlock(unit: ViewCompilationUnit, boundaryBlock: t.Boundar
637639
primaryCreateOp.handle,
638640
null,
639641
);
640-
conditions.push(primaryCaseExpr);
642+
641643
ingestNodes(primaryView, boundaryBlock.children);
642644

643645
unit.update.push(
644-
ir.createBoundaryOp(createOp.xref, primaryCreateOp.xref, conditions, boundaryBlock.sourceSpan),
646+
ir.createBoundaryOp(
647+
createOp.xref,
648+
primaryCreateOp.xref,
649+
primaryCaseExpr,
650+
conditions,
651+
boundaryBlock.sourceSpan,
652+
),
645653
);
646654
}
647655

packages/compiler/src/template/pipeline/src/phases/boundary_conditions.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,33 @@ export function generateBoundaryConditions(job: ComponentCompilationJob): void {
3737
console.log(' Condition target:', c.target);
3838
}
3939

40-
const primaryBranch = op.conditions.find((c) => c.target === op.primaryTarget);
41-
const errorBranch = op.conditions.find((c) => c.target !== op.primaryTarget);
40+
const errorBranches = op.conditions;
41+
const fallbackBranch = errorBranches.find((c) => c.expr === null);
42+
let errorResultExpr: o.Expression = fallbackBranch
43+
? new ir.SlotLiteralExpr(fallbackBranch.targetSlot)
44+
: o.literal(-1);
4245

43-
if (!primaryBranch || !errorBranch) {
44-
throw new Error(`Boundary must have both primary and error branches`);
46+
// Iterate in reverse order over error branches WITH conditions
47+
const conditionalBranches = errorBranches.filter((c) => c.expr !== null);
48+
for (let i = conditionalBranches.length - 1; i >= 0; i--) {
49+
const branch = conditionalBranches[i];
50+
51+
let conditionExpr = branch.expr!;
52+
53+
// The alias variable for the condition is generated in generate_variables.ts
54+
// and resolved by resolve_names.
55+
56+
errorResultExpr = new o.ConditionalExpr(
57+
conditionExpr,
58+
new ir.SlotLiteralExpr(branch.targetSlot),
59+
errorResultExpr,
60+
);
4561
}
4662

4763
op.processed = new o.ConditionalExpr(
4864
condition,
49-
new ir.SlotLiteralExpr(primaryBranch.targetSlot),
50-
new ir.SlotLiteralExpr(errorBranch.targetSlot),
65+
new ir.SlotLiteralExpr(op.guarded.targetSlot),
66+
errorResultExpr,
5167
);
5268
}
5369
}

packages/compiler/src/template/pipeline/src/phases/conditionals.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {ComponentCompilationJob} from '../compilation';
1616
export function generateConditionalExpressions(job: ComponentCompilationJob): void {
1717
for (const unit of job.units) {
1818
for (const op of unit.ops()) {
19-
if (op.kind !== ir.OpKind.Conditional && op.kind !== ir.OpKind.Boundary) {
19+
if (op.kind !== ir.OpKind.Conditional) {
2020
continue;
2121
}
2222

packages/compiler/src/template/pipeline/src/phases/generate_variables.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function recursivelyProcessView(view: ViewCompilationUnit, parentScope: Scope |
4545
switch (op.kind) {
4646
case ir.OpKind.ConditionalCreate:
4747
case ir.OpKind.ConditionalBranchCreate:
48+
case ir.OpKind.BoundaryErrorCreate:
4849
case ir.OpKind.Template:
4950
// Descend into child embedded views.
5051
recursivelyProcessView(view.job.views.get(op.xref)!, scope);
@@ -238,6 +239,22 @@ function getScopeForView(view: ViewCompilationUnit, parent: Scope | null): Scope
238239
},
239240
});
240241
break;
242+
243+
case ir.OpKind.BoundaryErrorCreate:
244+
const boundaryStateExpr = new ir.BoundaryStateExpr(op.boundaryXref);
245+
const errorProp = new o.ReadPropExpr(boundaryStateExpr, 'error');
246+
247+
for (const variable of op.contextVariables) {
248+
if (variable.value === '$error') {
249+
view.aliases.add({
250+
kind: ir.SemanticVariableKind.Alias,
251+
name: null,
252+
identifier: variable.name,
253+
expression: errorProp,
254+
});
255+
}
256+
}
257+
break;
241258
}
242259
}
243260

packages/compiler/src/template/pipeline/src/phases/naming.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ function addNamesToView(unit: CompilationUnit, baseName: string, state: {index:
143143
break;
144144
case ir.OpKind.ConditionalCreate:
145145
case ir.OpKind.ConditionalBranchCreate:
146+
case ir.OpKind.BoundaryErrorCreate:
146147
case ir.OpKind.Template:
147148
if (!(unit instanceof ViewCompilationUnit)) {
148149
throw new Error(`AssertionError: must be compiling a component`);

0 commit comments

Comments
 (0)