Repository: cdklabs/decdk
Pull request #412 (2022-10-26 08:14:55)

feat: collect errors
Resolves first part of #410 
Laying the groudwork and collect evaluation errors.

---

Creates errors like below. Note that the formatting is not final. We should probably do some tree based output.
The errors can also be inspected programatically.

```
DeclarativeStackError:

[RuntimeError at Resources.SiteDistribution]
    Cannot read properties of undefined (reading 'bind')

[TypeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin"]
    Cannot read properties of undefined (reading 'isWebsite')

[RuntimeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin".0.Ref]
    No such Resource or Parameter: SiteCertificate

[RuntimeError at Resources.SiteDistribution.Properties.defaultBehavior.origin."aws-cdk-lib.aws_cloudfront_origins.S3Origin".1.originAccessIdentity.Ref]
    No such Resource or Parameter: CloudFrontOAI

[RuntimeError at Resources.SiteDistribution.Properties.certificate.Ref]
    No such Resource or Parameter: SiteBucket

[RuntimeError at Resources.SiteDistribution.Properties.domainNames.0.Ref]
    No such Resource or Parameter: DomainName
```



Patch:
diff --git a/src/decdk.ts b/src/decdk.ts
index 2ca0092..66fd79f 100644
--- a/src/decdk.ts
+++ b/src/decdk.ts
@@ -2,16 +2,26 @@ import * as cdk from 'aws-cdk-lib';
 import chalk from 'chalk';
 import yargs from 'yargs';
 import { DeclarativeStack } from './declarative-stack';
+import { DeclarativeStackError } from './error-handling';
 import { loadTypeSystem, readTemplate, stackNameFromFileName } from './util';
 
+let verbosity = 0;
+
 async function main() {
   const argv = await yargs
     .usage(
       '$0 <filename>',
       'Synthesize a CDK stack from a declarative JSON or YAML template'
     )
+    .option('verbose', {
+      alias: 'v',
+      type: 'count',
+      description: 'Show debug output. Repeat to increase verbosity.',
+    })
     .positional('filename', { type: 'string', required: true }).argv;
 
+  verbosity = argv.verbose;
+
   const templateFile = argv.filename;
   if (!templateFile) {
     throw new Error('filename is missing');
@@ -35,6 +45,9 @@ async function main() {
 }
 
 main().catch((e) => {
+  if (e instanceof DeclarativeStackError) {
+    e = e.toString(verbosity >= 1);
+  }
   // eslint-disable-next-line no-console
   console.error(chalk.red(e));
   process.exit(1);
diff --git a/src/declarative-stack.ts b/src/declarative-stack.ts
index 9fee685..e974724 100644
--- a/src/declarative-stack.ts
+++ b/src/declarative-stack.ts
@@ -1,6 +1,7 @@
 import * as cdk from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 import * as reflect from 'jsii-reflect';
+import { AnnotationsContext, DeclarativeStackError } from './error-handling';
 import { EvaluationContext, Evaluator } from './evaluate';
 import { Template } from './parser/template';
 import { TypedTemplate } from './type-resolution/template';
@@ -37,6 +38,7 @@ export class DeclarativeStack extends cdk.Stack {
       };
     })(this, '$decdkAnalytics');
 
+    const annotations = AnnotationsContext.root();
     const context = new EvaluationContext({
       stack: this,
       template,
@@ -45,8 +47,12 @@ export class DeclarativeStack extends cdk.Stack {
     const ev = new Evaluator(context);
 
     _cwd(props.workingDirectory, () => {
-      ev.evaluateTemplate();
+      ev.evaluateTemplate(annotations);
     });
+
+    if (annotations.hasErrors()) {
+      throw new DeclarativeStackError(annotations);
+    }
   }
 }
 
diff --git a/src/error-handling/annotations.ts b/src/error-handling/annotations.ts
new file mode 100644
index 0000000..eda8bd3
--- /dev/null
+++ b/src/error-handling/annotations.ts
@@ -0,0 +1,56 @@
+import { AnnotatedError } from './errors';
+
+export class AnnotationsContext {
+  public static root(): AnnotationsContext {
+    return new AnnotationsContext();
+  }
+
+  public readonly children = new Array<AnnotationsContext>();
+  public readonly path: Array<string | number> = [];
+  public readonly parent?: AnnotationsContext;
+  private readonly _errorsStack: AnnotatedError[] = [];
+
+  private constructor(parent?: AnnotationsContext, path?: string | number) {
+    if (parent) {
+      this.path.push(...parent.path);
+      this.parent = parent;
+      parent.children.push(this);
+    }
+
+    if (path !== undefined) {
+      this.path.push(path);
+    }
+  }
+
+  public get root(): boolean {
+    return !!this.parent;
+  }
+
+  public hasErrors(): boolean {
+    return this.errors.length > 0;
+  }
+
+  public get errors(): AnnotatedError[] {
+    return [...this._errorsStack, ...this.children.flatMap((c) => c.errors)];
+  }
+
+  public child(path: string | number): AnnotationsContext {
+    return new AnnotationsContext(this, path);
+  }
+
+  public wrap<T>(fn: (ctx: AnnotationsContext) => T): T | void {
+    try {
+      return fn(this);
+    } catch (error) {
+      this.error(error as any);
+    }
+  }
+
+  public error(error: Error) {
+    this._errorsStack.push(new AnnotatedError(this.path, error));
+  }
+
+  public toString(printStackStrace = false) {
+    return this.errors.map((e) => e.toString(printStackStrace)).join('\n\n');
+  }
+}
diff --git a/src/error-handling/errors.ts b/src/error-handling/errors.ts
new file mode 100644
index 0000000..f5ebb60
--- /dev/null
+++ b/src/error-handling/errors.ts
@@ -0,0 +1,68 @@
+import { AnnotationsContext } from './annotations';
+
+function indent(text: string, spaces = 4) {
+  const TAB = ' '.repeat(spaces);
+  return text
+    .split('\n')
+    .map((l) => TAB + l)
+    .join('\n');
+}
+
+export class DeclarativeStackError extends Error {
+  constructor(public readonly annotations: AnnotationsContext) {
+    super();
+    this.name = 'DeclarativeStackError';
+    this.message = this.toString();
+    Object.setPrototypeOf(this, DeclarativeStackError.prototype);
+  }
+
+  public toString(debug = false) {
+    return `${this.name}:\n\n${this.annotations.toString(debug)}`;
+  }
+}
+
+/**
+ * Thrown by the Evaluator
+ */
+export class RuntimeError extends Error {
+  public static wrap(error: any) {
+    return new RuntimeError(error.message, error.stack);
+  }
+
+  constructor(message: string, stack?: string) {
+    super(message);
+    this.name = 'RuntimeError';
+    if (stack) {
+      this.stack = stack;
+    }
+    Object.setPrototypeOf(this, RuntimeError.prototype);
+  }
+}
+
+/**
+ * Annotation any Error with additional info
+ */
+export class AnnotatedError {
+  constructor(
+    public readonly stack: Array<string | number>,
+    public readonly error: Error
+  ) {}
+
+  public toString(printStackStrace = false) {
+    const details = this.renderErrorDetails(printStackStrace);
+    return `[${this.error.name} at ${this.renderStack()}]\n${indent(details)}`;
+  }
+
+  protected renderErrorDetails(printStackStrace = false): string {
+    if (printStackStrace) {
+      return this.error.stack ?? this.error.message;
+    }
+    return this.error.message;
+  }
+
+  protected renderStack(): string {
+    return this.stack
+      .map((s) => (s.toString().includes('.') ? `"${s}"` : s))
+      .join('.');
+  }
+}
diff --git a/src/error-handling/index.ts b/src/error-handling/index.ts
new file mode 100644
index 0000000..0ec7e4d
--- /dev/null
+++ b/src/error-handling/index.ts
@@ -0,0 +1,3 @@
+export * from './annotations';
+export * from './errors';
+export * from './unparse';
diff --git a/src/error-handling/unparse.ts b/src/error-handling/unparse.ts
new file mode 100644
index 0000000..c6e33ec
--- /dev/null
+++ b/src/error-handling/unparse.ts
@@ -0,0 +1,28 @@
+import {
+  INTRINSIC_NAME_MAP,
+  TemplateExpression,
+  UserIntrinsicExpression,
+} from '../parser/template';
+
+export function unparseExpression(x: TemplateExpression): any {
+  switch (x.type) {
+    case 'string':
+    case 'number':
+    case 'boolean':
+      return x.value;
+    case 'null':
+      return null;
+    case 'array':
+      return x.array.map(unparseExpression);
+    case 'object':
+      return Object.fromEntries(
+        Object.entries(x.fields).map(([k, v]) => [k, unparseExpression(v)])
+      );
+    default:
+      return x;
+  }
+}
+
+export function intrinsicToLongForm(fn: UserIntrinsicExpression['fn']): string {
+  return INTRINSIC_NAME_MAP?.[fn] ?? fn;
+}
diff --git a/src/evaluate/context.ts b/src/evaluate/context.ts
index 10a5b42..48dae0e 100644
--- a/src/evaluate/context.ts
+++ b/src/evaluate/context.ts
@@ -1,5 +1,6 @@
 import * as cdk from 'aws-cdk-lib';
 import * as reflect from 'jsii-reflect';
+import { RuntimeError } from '../error-handling';
 import { TypedTemplate } from '../type-resolution/template';
 import { InstanceReference, Reference, References } from './references';
 
@@ -47,7 +48,7 @@ export class EvaluationContext {
   public reference(logicalId: string): Reference {
     const r = this.availableRefs.get(logicalId);
     if (!r) {
-      throw new Error(`No resource or parameter with name: ${logicalId}`);
+      throw new RuntimeError(`No such Resource or Parameter: ${logicalId}`);
     }
     return r;
   }
@@ -55,7 +56,7 @@ export class EvaluationContext {
   public mapping(mappingName: string) {
     const map = this.template?.mappings?.get(mappingName);
     if (!map) {
-      throw new Error(`No such Mapping: ${mappingName}`);
+      throw new RuntimeError(`No such Mapping: ${mappingName}`);
     }
     return map;
   }
@@ -63,7 +64,7 @@ export class EvaluationContext {
   public condition(conditionName: string) {
     const condition = this.template?.conditions?.get(conditionName);
     if (!condition) {
-      throw new Error(`No such condition: ${conditionName}`);
+      throw new RuntimeError(`No such condition: ${conditionName}`);
     }
     return condition;
   }
@@ -84,7 +85,7 @@ export class EvaluationContext {
       }
       curr = curr[next];
       if (!curr) {
-        throw new Error(`unable to resolve class ${className}`);
+        throw new RuntimeError(`Class not found: ${fqn}`);
       }
     }
 
diff --git a/src/evaluate/evaluate.ts b/src/evaluate/evaluate.ts
index b2f2db0..07c8a65 100644
--- a/src/evaluate/evaluate.ts
+++ b/src/evaluate/evaluate.ts
@@ -9,10 +9,16 @@ import {
   Token,
 } from 'aws-cdk-lib';
 import { Construct, IConstruct } from 'constructs';
+import {
+  AnnotationsContext,
+  RuntimeError,
+  intrinsicToLongForm,
+} from '../error-handling';
 import { SubFragment } from '../parser/private/sub';
 import { assertString } from '../parser/private/types';
 import {
   GetPropIntrinsic,
+  IntrinsicExpression,
   LazyLogicalId,
   RefIntrinsic,
 } from '../parser/template';
@@ -29,7 +35,10 @@ import {
   InstanceMethodCallExpression,
   StaticMethodCallExpression,
 } from '../type-resolution/callables';
-import { TypedTemplateExpression } from '../type-resolution/expression';
+import {
+  TypedArrayExpression,
+  TypedTemplateExpression,
+} from '../type-resolution/expression';
 import { ResolveReferenceExpression } from '../type-resolution/references';
 import { EvaluationContext } from './context';
 import { DeCDKCfnOutput } from './outputs';
@@ -45,96 +54,112 @@ import {
 export class Evaluator {
   constructor(public readonly context: EvaluationContext) {}
 
-  public evaluateTemplate() {
-    this.evaluateParameters();
-    this.evaluateMetadata();
-    this.evaluateRules();
-    this.evaluateMappings();
-    this.evaluateConditions();
-    this.evaluateTransform();
-    this.evaluateResources();
-    this.evaluateOutputs();
-    this.evaluateHooks();
+  public evaluateTemplate(ctx: AnnotationsContext) {
+    this.evaluateParameters(ctx.child('Parameters'));
+    this.evaluateMetadata(ctx.child('Metadata'));
+    this.evaluateRules(ctx.child('Rules'));
+    this.evaluateMappings(ctx.child('Mappings'));
+    this.evaluateConditions(ctx.child('Conditions'));
+    this.evaluateTransform(ctx.child('Transform'));
+    this.evaluateResources(ctx.child('Resources'));
+    this.evaluateOutputs(ctx.child('Outputs'));
+    this.evaluateHooks(ctx.child('Hooks'));
   }
 
-  private evaluateMappings() {
+  private evaluateMappings(ctx: AnnotationsContext) {
     const scope = new Construct(this.context.stack, '$Mappings');
     this.context.template.mappings.forEach((mapping, mapName) =>
-      new cdk.CfnMapping(scope, mapName, {
-        mapping: mapping.toObject(),
-      }).overrideLogicalId(mapName)
+      ctx.child(mapName).wrap(() => {
+        new cdk.CfnMapping(scope, mapName, {
+          mapping: mapping.toObject(),
+        }).overrideLogicalId(mapName);
+      })
     );
   }
 
-  private evaluateParameters() {
+  private evaluateParameters(ctx: AnnotationsContext) {
     this.context.template.parameters.forEach((param, paramName) => {
-      new cdk.CfnParameter(
-        this.context.stack,
-        paramName,
-        param
-      ).overrideLogicalId(paramName);
-      this.context.addReference(new SimpleReference(paramName));
+      ctx.child(paramName).wrap(() => {
+        new cdk.CfnParameter(
+          this.context.stack,
+          paramName,
+          param
+        ).overrideLogicalId(paramName);
+        this.context.addReference(new SimpleReference(paramName));
+      });
     });
   }
 
-  private evaluateResources() {
-    this.context.template.resources.forEach((_logicalId, resource) =>
-      this.evaluateResource(resource)
+  private evaluateResources(ctx: AnnotationsContext) {
+    this.context.template.resources.forEach((logicalId, resource) =>
+      ctx.child(logicalId).wrap((c) => this.evaluateResource(resource, c))
     );
   }
 
-  private evaluateOutputs() {
+  private evaluateOutputs(ctx: AnnotationsContext) {
     const scope = new Construct(this.context.stack, '$Outputs');
     this.context.template.outputs.forEach((output, outputId) => {
-      new DeCDKCfnOutput(scope, outputId, {
-        value: this.evaluate(output.value),
-        description: output.description,
-        exportName: output.exportName
-          ? this.evaluate(output.exportName)
-          : output.exportName,
-        condition: output.conditionName,
-      }).overrideLogicalId(outputId);
+      ctx.child(outputId).wrap(() => {
+        new DeCDKCfnOutput(scope, outputId, {
+          value: this.evaluate(output.value, ctx),
+          description: output.description,
+          exportName: output.exportName
+            ? this.evaluate(output.exportName, ctx)
+            : output.exportName,
+          condition: output.conditionName,
+        }).overrideLogicalId(outputId);
+      });
     });
   }
 
-  private evaluateTransform() {
-    this.context.template.transform.forEach((t) => {
-      this.context.stack.addTransform(t);
+  private evaluateTransform(ctx: AnnotationsContext) {
+    ctx.wrap(() => {
+      this.context.template.transform.forEach((t) => {
+        this.context.stack.addTransform(t);
+      });
     });
   }
 
-  private evaluateMetadata() {
+  private evaluateMetadata(ctx: AnnotationsContext) {
     this.context.template.metadata.forEach((v, k) => {
-      this.context.stack.addMetadata(k, v);
+      ctx.child(k).wrap(() => {
+        this.context.stack.addMetadata(k, v);
+      });
     });
   }
 
-  private evaluateRules() {
+  private evaluateRules(ctx: AnnotationsContext) {
     const scope = new Construct(this.context.stack, '$Rules');
     this.context.template.rules.forEach((rule, name) => {
-      new CfnRule(scope, name, rule).overrideLogicalId(name);
+      ctx.child(name).wrap(() => {
+        new CfnRule(scope, name, rule).overrideLogicalId(name);
+      });
     });
   }
 
-  private evaluateHooks() {
+  private evaluateHooks(ctx: AnnotationsContext) {
     const scope = new Construct(this.context.stack, '$Hooks');
     this.context.template.hooks.forEach((hook, name) => {
-      new CfnHook(scope, name, hook).overrideLogicalId(name);
+      ctx.child(name).wrap(() => {
+        new CfnHook(scope, name, hook).overrideLogicalId(name);
+      });
     });
   }
 
-  private evaluateConditions() {
+  private evaluateConditions(ctx: AnnotationsContext) {
     const scope = new Construct(this.context.stack, '$Conditions');
     this.context.template.conditions.forEach((condition, logicalId) => {
-      const conditionFn = this.evaluate(condition);
-      new cdk.CfnCondition(scope, logicalId, {
-        expression: conditionFn,
-      }).overrideLogicalId(logicalId);
+      ctx.child(logicalId).wrap(() => {
+        const conditionFn = this.evaluate(condition, ctx);
+        new cdk.CfnCondition(scope, logicalId, {
+          expression: conditionFn,
+        }).overrideLogicalId(logicalId);
+      });
     });
   }
 
-  public evaluateResource(resource: ResourceLike) {
-    const construct = this.evaluate(resource);
+  public evaluateResource(resource: ResourceLike, ctx: AnnotationsContext) {
+    const construct = this.evaluate(resource, ctx);
 
     // If this is the result of a call to a method with no
     // return type (void), then there is nothing else to do here.
@@ -143,7 +168,11 @@ export class Evaluator {
     this.applyTags(construct, resource.tags);
     this.applyDependsOn(construct, resource.dependsOn);
     if (isCdkConstructExpression(resource)) {
-      this.applyOverrides(construct, resource.overrides);
+      this.applyOverrides(
+        construct,
+        resource.overrides,
+        ctx.child('Overrides')
+      );
     }
 
     this.context.addReference(
@@ -163,126 +192,71 @@ export class Evaluator {
     return new ConstructReference(logicalId, value as Construct);
   }
 
-  public evaluate(x: TypedTemplateExpression): any {
-    const ev = this.evaluate.bind(this);
-    const maybeEv = (y?: TypedTemplateExpression): any =>
-      y ? ev(y) : undefined;
-
-    switch (x.type) {
-      case 'null':
-        return undefined;
-      case 'string':
-      case 'number':
-      case 'boolean':
-        return x.value;
-      case 'date':
-        return x.date;
-      case 'array':
-        return this.evaluateArray(x.array);
-      case 'struct':
-      case 'object':
-        return this.evaluateObject(x.fields);
-      case 'resolve-reference':
-        return this.resolveReference(x.reference);
-      case 'intrinsic':
-        switch (x.fn) {
-          case 'base64':
-            return this.fnBase64(assertString(ev(x.expression)));
-          case 'cidr':
-            return this.fnCidr(ev(x.ipBlock), ev(x.count), maybeEv(x.netMask));
-          case 'findInMap':
-            return this.fnFindInMap(
-              assertString(ev(x.mappingName)),
-              assertString(ev(x.key1)),
-              assertString(ev(x.key2))
+  public evaluate(x: TypedTemplateExpression, ctx: AnnotationsContext): any {
+    try {
+      switch (x.type) {
+        case 'null':
+          return undefined;
+        case 'string':
+        case 'number':
+        case 'boolean':
+          return x.value;
+        case 'date':
+          return x.date;
+        case 'array':
+          return this.evaluateArray(x.array, ctx);
+        case 'struct':
+        case 'object':
+          return this.evaluateObject(x.fields, ctx);
+        case 'resolve-reference':
+          return this.resolveReference(x.reference, ctx);
+        case 'intrinsic':
+          return this.evaluateIntrinsic(x, ctx);
+        case 'enum':
+          return this.enum(x.fqn, x.choice);
+        case 'staticProperty':
+          return this.enum(x.fqn, x.property);
+        case 'any':
+          return this.evaluate(x.value, ctx);
+        case 'void':
+          return;
+        case 'lazyResource':
+          return this.invoke(x.call, ctx.child('Call'));
+        case 'construct':
+          return this.initializeConstruct(x, ctx);
+        case 'cdkObject':
+          return this.initializeCdkObject(x, ctx);
+        case 'resource':
+          return this.initializeCfnResource(x, ctx);
+        case 'initializer':
+          return ctx
+            .child(x.fqn)
+            .wrap((innerCtx) =>
+              this.initializer(
+                x.fqn,
+                this.evaluateArray(x.args.array, innerCtx)
+              )
             );
-          case 'getAtt':
-            return this.fnGetAtt(x.logicalId, assertString(ev(x.attribute)));
-          case 'getProp':
-            return this.fnGetProp(x.logicalId, assertString(x.property));
-          case 'getAzs':
-            return this.fnGetAzs(assertString(ev(x.region)));
-          case 'if':
-            return this.fnIf(x.conditionName, x.then, x.else);
-          case 'importValue':
-            return this.fnImportValue(assertString(ev(x.export)));
-          case 'join':
-            return this.fnJoin(assertString(x.separator), ev(x.list));
-          case 'ref':
-            return this.cfnRef(x.logicalId);
-          case 'select':
-            return this.fnSelect(ev(x.index), ev(x.objects));
-          case 'split':
-            return this.fnSplit(x.separator, assertString(ev(x.value)));
-          case 'sub':
-            return this.fnSub(
-              x.fragments,
-              this.evaluateObject(x.additionalContext)
-            );
-          case 'transform':
-            return this.fnTransform(
-              x.transformName,
-              this.evaluateObject(x.parameters)
-            );
-          case 'and':
-            return this.fnAnd(x.operands.map(ev));
-          case 'or':
-            return this.fnOr(x.operands.map(ev));
-          case 'not':
-            return this.fnNot(ev(x.operand));
-          case 'equals':
-            return this.fnEquals(ev(x.value1), ev(x.value2));
-          case 'args':
-            return this.evaluateArray(x.array);
-          case 'lazyLogicalId':
-            return this.lazyLogicalId(x);
-          case 'length':
-            return this.fnLength(ev(x.list));
-          case 'toJsonString':
-            return this.toJsonString(this.evaluateObject(x.value));
-        }
-      case 'enum':
-        return this.enum(x.fqn, x.choice);
-      case 'staticProperty':
-        return this.enum(x.fqn, x.property);
-      case 'any':
-        return ev(x.value);
-      case 'void':
-        return;
-      case 'lazyResource':
-        return this.invoke(x.call);
-      case 'construct':
-        return this.initializeConstruct(x, ev);
-      case 'cdkObject':
-        return this.initializeCdkObject(x, ev);
-      case 'resource':
-        return this.initializeCfnResource(x, ev);
-      case 'initializer':
-        return this.initializer(x.fqn, this.evaluateArray(x.args.array));
-      case 'staticMethodCall':
-        return this.invokeStaticMethod(
-          x.fqn,
-          x.method,
-          this.evaluateArray(x.args.array)
-        );
+        case 'staticMethodCall':
+          return this.invoke(x, ctx);
+      }
+    } catch (error) {
+      ctx.error(RuntimeError.wrap(error));
     }
   }
 
-  protected initializeCfnResource(
-    x: CfnResourceNode,
-    ev: (x: TypedTemplateExpression) => any
-  ) {
+  protected initializeCfnResource(x: CfnResourceNode, ctx: AnnotationsContext) {
     const resource = this.initializer(x.fqn, [
       this.context.stack,
       x.logicalId,
-      ev(x.props),
+      this.evaluate(x.props, ctx.child('Properties')),
     ]) as CfnResource;
 
     resource.cfnOptions.creationPolicy = x.creationPolicy
-      ? ev(x.creationPolicy)
+      ? this.evaluate(x.creationPolicy, ctx.child('CreationPolicy'))
       : undefined;
     resource.cfnOptions.updatePolicy = x.updatePolicy
-      ? ev(x.updatePolicy)
+      ? this.evaluate(x.updatePolicy, ctx.child('UpdatePolicy'))
       : undefined;
     resource.cfnOptions.metadata = x.metadata;
     resource.cfnOptions.updateReplacePolicy =
@@ -292,22 +266,18 @@ export class Evaluator {
     return resource;
   }
 
-  protected initializeConstruct(
-    x: CdkConstruct,
-    ev: (x: TypedTemplateExpression) => any
-  ) {
+  protected initializeConstruct(x: CdkConstruct, ctx: AnnotationsContext) {
     return this.initializer(x.fqn, [
       this.context.stack,
       x.logicalId,
-      ev(x.props),
+      this.evaluate(x.props, ctx.child('Properties')),
     ]);
   }
 
-  protected initializeCdkObject(
-    x: CdkObject,
-    ev: (x: TypedTemplateExpression) => any
-  ) {
-    return this.initializer(x.fqn, [ev(x.props)]);
+  protected initializeCdkObject(x: CdkObject, ctx: AnnotationsContext) {
+    return this.initializer(x.fqn, [
+      this.evaluate(x.props, ctx.child('Properties')),
+    ]);
   }
 
   protected lazyLogicalId(x: LazyLogicalId) {
@@ -326,20 +296,88 @@ export class Evaluator {
   }
 
   public evaluateObject(
-    xs: Record<string, TypedTemplateExpression>
+    xs: Record<string, TypedTemplateExpression>,
+    ctx: AnnotationsContext
   ): Record<string, unknown> {
     return Object.fromEntries(
-      Object.entries(xs).map(([k, v]) => [k, this.evaluate(v)])
+      Object.entries(xs).map(([k, v]) => [k, this.evaluate(v, ctx.child(k))])
     );
   }
 
-  public evaluateArray(xs: TypedTemplateExpression[]) {
-    return xs.map(this.evaluate.bind(this));
+  public evaluateArray(xs: TypedTemplateExpression[], ctx: AnnotationsContext) {
+    return xs.map((x, idx) => this.evaluate(x, ctx.child(idx)));
   }
 
-  public evaluateCondition(conditionName: string) {
+  public evaluateIntrinsic(x: IntrinsicExpression, ctx: AnnotationsContext) {
+    if (x.fn === 'lazyLogicalId') {
+      return this.lazyLogicalId(x);
+    }
+
+    return ctx.child(intrinsicToLongForm(x.fn)).wrap((intrinsicCtx) => {
+      const ev = (y: TypedTemplateExpression) => this.evaluate(y, intrinsicCtx);
+      const maybeEv = (y?: TypedTemplateExpression): any =>
+        y ? ev(y) : undefined;
+
+      switch (x.fn) {
+        case 'base64':
+          return this.fnBase64(assertString(ev(x.expression)));
+        case 'cidr':
+          return this.fnCidr(ev(x.ipBlock), ev(x.count), maybeEv(x.netMask));
+        case 'findInMap':
+          return this.fnFindInMap(
+            assertString(ev(x.mappingName)),
+            assertString(ev(x.key1)),
+            assertString(ev(x.key2))
+          );
+        case 'getAtt':
+          return this.fnGetAtt(x.logicalId, assertString(ev(x.attribute)));
+        case 'getProp':
+          return this.fnGetProp(x.logicalId, assertString(x.property));
+        case 'getAzs':
+          return this.fnGetAzs(assertString(ev(x.region)));
+        case 'if':
+          return this.fnIf(x.conditionName, x.then, x.else, intrinsicCtx);
+        case 'importValue':
+          return this.fnImportValue(assertString(ev(x.export)));
+        case 'join':
+          return this.fnJoin(assertString(x.separator), ev(x.list));
+        case 'ref':
+          return this.cfnRef(x.logicalId);
+        case 'select':
+          return this.fnSelect(ev(x.index), ev(x.objects));
+        case 'split':
+          return this.fnSplit(x.separator, assertString(ev(x.value)));
+        case 'sub':
+          return this.fnSub(
+            x.fragments,
+            this.evaluateObject(x.additionalContext, intrinsicCtx)
+          );
+        case 'transform':
+          return this.fnTransform(
+            x.transformName,
+            this.evaluateObject(x.parameters, intrinsicCtx)
+          );
+        case 'and':
+          return this.fnAnd(x.operands.map((o) => ev(o)));
+        case 'or':
+          return this.fnOr(x.operands.map((o) => ev(o)));
+        case 'not':
+          return this.fnNot(ev(x.operand));
+        case 'equals':
+          return this.fnEquals(ev(x.value1), ev(x.value2));
+        case 'args':
+          return this.evaluateArray(x.array, intrinsicCtx);
+        case 'length':
+          return this.fnLength(ev(x.list));
+        case 'toJsonString':
+          return this.toJsonString(this.evaluateObject(x.value, intrinsicCtx));
+      }
+    });
+  }
+
+  public evaluateCondition(conditionName: string, ctx: AnnotationsContext) {
     const condition = this.context.condition(conditionName);
-    const result = this.evaluate(condition);
+    const result = this.evaluate(condition, ctx);
     if (typeof result !== 'boolean') {
       throw new Error(
         `Condition does not evaluate to boolean: ${JSON.stringify(result)}`
@@ -349,25 +387,47 @@ export class Evaluator {
   }
 
   protected invoke(
-    call: StaticMethodCallExpression | InstanceMethodCallExpression
+    call: StaticMethodCallExpression | InstanceMethodCallExpression,
+    ctx: AnnotationsContext
   ) {
-    const parameters = this.evaluateArray(call.args.array);
+    const evalParams = (
+      args: TypedArrayExpression,
+      innerCtx: AnnotationsContext
+    ) => this.evaluateArray(args.array, innerCtx.child('CDK::Args'));
+
+    if (call.type === 'staticMethodCall') {
+      return ctx.child(`${call.fqn}.${call.method}`).wrap((innerCtx) => {
+        return this.invokeStaticMethod(
+          call.fqn,
+          call.method,
+          evalParams(call.args, innerCtx)
+        );
+      });
+    }
 
-    return call.type === 'staticMethodCall'
-      ? this.invokeStaticMethod(call.fqn, call.method, parameters)
-      : this.invokeInstanceMethod(call.target, call.method, parameters);
+    return ctx
+      .child(`${call.target.reference}.${call.method}`)
+      .wrap((innerCtx) => {
+        return this.invokeInstanceMethod(
+          call.target,
+          call.method,
+          evalParams(call.args, innerCtx),
+          innerCtx
+        );
+      });
   }
 
   private invokeInstanceMethod(
     target: ResolveReferenceExpression,
     method: string,
-    parameters: any[]
+    parameters: any[],
+    ctx: AnnotationsContext
   ) {
-    const instance = this.resolveReference(target.reference);
+    const instance = this.resolveReference(target.reference, ctx);
     return instance[method](...parameters);
   }
 
-  protected invokeStaticMethod(
+  private invokeStaticMethod(
     fqn: string,
     method: string,
     parameters: unknown[]
@@ -429,12 +489,13 @@ export class Evaluator {
   protected fnIf(
     conditionName: string,
     ifYes: TypedTemplateExpression,
-    ifNo: TypedTemplateExpression
+    ifNo: TypedTemplateExpression,
+    ctx: AnnotationsContext
   ) {
     return cdk.Fn.conditionIf(
       conditionName,
-      this.evaluate(ifYes),
-      this.evaluate(ifNo)
+      this.evaluate(ifYes, ctx),
+      this.evaluate(ifNo, ctx)
     );
   }
 
@@ -446,20 +507,25 @@ export class Evaluator {
     return cdk.Fn.join(separator, array);
   }
 
-  protected resolveReference(intrinsic: RefIntrinsic | GetPropIntrinsic) {
+  protected resolveReference(
+    intrinsic: RefIntrinsic | GetPropIntrinsic,
+    ctx: AnnotationsContext
+  ) {
     const { logicalId, fn } = intrinsic;
 
-    const c = this.context.reference(logicalId);
-
     if (fn !== 'ref') {
-      return this.evaluate(intrinsic);
+      return this.evaluateIntrinsic(intrinsic, ctx);
     }
 
-    if (!c.instance) {
-      return this.cfnRef(logicalId);
-    }
+    return ctx.child('Ref').wrap(() => {
+      const c = this.context.reference(logicalId);
+
+      if (!c.instance) {
+        return this.cfnRef(logicalId);
+      }
 
-    return c.instance;
+      return c.instance;
+    });
   }
 
   protected cfnRef(logicalId: string) {
@@ -565,9 +631,10 @@ export class Evaluator {
 
   protected applyOverrides(
     resource: IConstruct,
-    overrides: ResourceOverride[]
+    overrides: ResourceOverride[],
+    ctx: AnnotationsContext
   ) {
-    const ev = this.evaluate.bind(this);
+    const ev = (x: TypedTemplateExpression) => this.evaluate(x, ctx);
     overrides.forEach((override: ResourceOverride) => {
       applyOverride(resource, override, ev);
     });
diff --git a/src/evaluate/references.ts b/src/evaluate/references.ts
index e7c1c5c..933ef57 100644
--- a/src/evaluate/references.ts
+++ b/src/evaluate/references.ts
@@ -1,7 +1,7 @@
 import * as cdk from 'aws-cdk-lib';
 import { Stack, Stage } from 'aws-cdk-lib';
 import { Construct } from 'constructs';
-import { assertObject, ParserError } from '../parser/private/types';
+import { assertObject } from '../parser/private/types';
 
 function isPropertyOf(
   instance: Record<string, unknown>,
@@ -21,7 +21,7 @@ export function getPropDot(instance: unknown, path: string): unknown {
     if (Array.isArray(o)) {
       const index = parseInt(p);
       if (isNaN(index) || !(0 <= index && index < o.length)) {
-        throw new ParserError(
+        throw new SyntaxError(
           `Expected an integer between 0 and ${o.length - 1}, got ${index}`
         );
       }
@@ -30,7 +30,7 @@ export function getPropDot(instance: unknown, path: string): unknown {
 
     const obj = assertObject(o);
     if (!isPropertyOf(obj, p)) {
-      throw new ParserError(`Expected Construct property path, got: ${path}`);
+      throw new SyntaxError(`Expected Construct property path, got: ${path}`);
     }
     return obj[p];
   }, instance);
diff --git a/src/parser/private/types.ts b/src/parser/private/types.ts
index e634cb0..d788399 100644
--- a/src/parser/private/types.ts
+++ b/src/parser/private/types.ts
@@ -1,16 +1,9 @@
 import { RetentionPolicy } from '../template';
 
-export class ParserError extends Error {
-  constructor(msg: string) {
-    super(msg);
-    Object.setPrototypeOf(this, ParserError.prototype);
-  }
-}
-
 export function parseNumber(asString: string | number) {
   const asNumber = parseInt(`${asString}`, 10);
   if (`${asNumber}` !== `${asString}`) {
-    throw new ParserError(`Not a number: ${asString}`);
+    throw new SyntaxError(`Not a number: ${asString}`);
   }
   return { asString: `${asNumber}`, asNumber };
 }
@@ -22,7 +15,7 @@ export function assertString(x: unknown): string {
   if (typeof x === 'string') {
     return x;
   }
-  throw new ParserError(`Expected string, got: ${JSON.stringify(x)}`);
+  throw new SyntaxError(`Expected string, got: ${JSON.stringify(x)}`);
 }
 
 export function assertNumber(x: unknown): number {
@@ -32,15 +25,15 @@ export function assertNumber(x: unknown): number {
   if (typeof x === 'string') {
     return parseNumber(x).asNumber;
   }
-  throw new ParserError(`Expected number, got: ${JSON.stringify(x)}`);
+  throw new SyntaxError(`Expected number, got: ${JSON.stringify(x)}`);
 }
 
 export function assertList<T = unknown>(x: unknown, lengths?: number[]): T[] {
   if (!Array.isArray(x)) {
-    throw new ParserError(`Expected list, got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected list, got: ${JSON.stringify(x)}`);
   }
   if (lengths && !lengths.includes(x.length)) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected list of length ${lengths}, got ${x.length}`
     );
   }
@@ -56,7 +49,7 @@ export function assertListOfForm<T>(
     return assertList(x).map(assertForm);
   } catch (e) {
     if (form) {
-      throw new ParserError(
+      throw new SyntaxError(
         `Expected list of form ${form}, got: ${JSON.stringify(x)}`
       );
     }
@@ -76,21 +69,21 @@ export function assertBoolean(x: unknown): boolean {
     case 'false':
       return false;
     default:
-      throw new ParserError(`Expected boolean, got: ${JSON.stringify(x)}`);
+      throw new SyntaxError(`Expected boolean, got: ${JSON.stringify(x)}`);
   }
 }
 
 export function assertTrue(x: unknown): true {
   assertBoolean(x);
   if (x !== true) {
-    throw new ParserError(`Expected 'true', got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected 'true', got: ${JSON.stringify(x)}`);
   }
   return x;
 }
 
 export function assertObject(x: unknown): Record<string, unknown> {
   if (typeof x !== 'object' || x == null || Array.isArray(x)) {
-    throw new ParserError(`Expected object, got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected object, got: ${JSON.stringify(x)}`);
   }
   return x as any;
 }
@@ -100,13 +93,13 @@ export function assertField<A extends object, K extends keyof A>(
   fieldName: K
 ): unknown {
   if (!(fieldName in xs)) {
-    throw new ParserError(`Expected field named '${String(fieldName)}'`);
+    throw new SyntaxError(`Expected field named '${String(fieldName)}'`);
   }
   return xs[fieldName];
 }
 export function assertOneOf<A extends unknown>(x: A, allowed: unknown[]): A {
   if (!allowed.includes(x)) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected one of ${allowed
         .map((f) => `'${String(f)}'`)
         .join('|')}, got: ${JSON.stringify(x)}`
@@ -121,7 +114,7 @@ export function assertExactlyOneOfFields<A extends object, K extends keyof A>(
 ): K {
   const foundFields = fieldNames.filter((f) => f in xs);
   if (foundFields.length !== 1) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected exactly one of the fields ${fieldNames
         .map((f) => `'${String(f)}'`)
         .join(', ')}, got: ${JSON.stringify(xs)}`
@@ -136,7 +129,7 @@ export function assertAtMostOneOfFields<A extends object, K extends keyof A>(
 ): K | undefined {
   const foundFields = fieldNames.filter((f) => f in xs);
   if (foundFields.length > 1) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected at most one of the fields ${fieldNames
         .map((f) => `'${String(f)}'`)
         .join(', ')}, got: ${JSON.stringify(xs)}`
@@ -148,7 +141,7 @@ export function assertAtMostOneOfFields<A extends object, K extends keyof A>(
 export function assertOneField(xs: unknown): string {
   const fields = Object.keys(assertObject(xs));
   if (fields.length !== 1) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected exactly one field, got: ${JSON.stringify(xs)}`
     );
   }
@@ -162,13 +155,13 @@ export function assertStringOrListIntoList(x: unknown): string[] {
   if (Array.isArray(x)) {
     const nonStrings = x.filter((y) => typeof y !== 'string');
     if (nonStrings.length > 0) {
-      throw new ParserError(
+      throw new SyntaxError(
         `Expected all strings in array, found: ${nonStrings}`
       );
     }
     return x as string[];
   }
-  throw new ParserError(
+  throw new SyntaxError(
     `Expected string or list of strings, got: ${JSON.stringify(x)}`
   );
 }
@@ -187,7 +180,7 @@ export function parseRetentionPolicy(x: unknown): RetentionPolicy {
       return 'Snapshot';
   }
 
-  throw new ParserError(
+  throw new SyntaxError(
     `Expected one of Delete|Retain|Snapshot, got: ${JSON.stringify(x)}`
   );
 }
@@ -212,7 +205,7 @@ export function assertOr<T>(
     } catch {}
   }
 
-  throw new ParserError(error(JSON.stringify(x)));
+  throw new SyntaxError(error(JSON.stringify(x)));
 }
 
 export function assertStringOrStringList(xs: unknown): string | string[] {
diff --git a/src/parser/template/calls.ts b/src/parser/template/calls.ts
index bb7a4b3..69ab81f 100644
--- a/src/parser/template/calls.ts
+++ b/src/parser/template/calls.ts
@@ -1,4 +1,4 @@
-import { assertOneField, assertString, ParserError } from '../private/types';
+import { assertOneField, assertString } from '../private/types';
 import {
   ArrayLiteral,
   parseExpression,
@@ -21,7 +21,7 @@ export function parseCall(x: unknown): FactoryMethodCall | undefined {
     case 2:
       return parseInstanceCall(array);
     default:
-      throw new ParserError(
+      throw new SyntaxError(
         `Method calls should have 1 or 2 elements, got ${array}`
       );
   }
diff --git a/src/parser/template/expression.ts b/src/parser/template/expression.ts
index eb972e8..8a9ef90 100644
--- a/src/parser/template/expression.ts
+++ b/src/parser/template/expression.ts
@@ -4,7 +4,6 @@ import {
   assertList,
   assertObject,
   assertString,
-  ParserError,
 } from '../private/types';
 
 export type TemplateExpression =
@@ -48,7 +47,9 @@ export interface ArrayExpression<T> {
 export interface ArrayLiteral extends ArrayExpression<TemplateExpression> {}
 export interface ObjectLiteral extends ObjectExpression<TemplateExpression> {}
 
-export type IntrinsicExpression =
+export type IntrinsicExpression = UserIntrinsicExpression | LazyLogicalId;
+
+export type UserIntrinsicExpression =
   | RefIntrinsic
   | GetAttIntrinsic
   | GetPropIntrinsic
@@ -68,7 +69,6 @@ export type IntrinsicExpression =
   | NotIntrinsic
   | EqualsIntrinsic
   | ArgsIntrinsic
-  | LazyLogicalId
   | LengthIntrinsic
   | ToJsonStringIntrinsic;
 
@@ -219,6 +219,33 @@ export interface ToJsonStringIntrinsic {
   readonly value: Record<string, TemplateExpression>;
 }
 
+export const INTRINSIC_NAME_MAP: Record<
+  Exclude<IntrinsicExpression['fn'], 'lazyLogicalId'>,
+  string
+> = {
+  ref: 'Ref',
+  getAtt: 'Fn::GetAtt',
+  getProp: 'CDK::GetProp',
+  base64: 'Fn::Base64',
+  cidr: 'Fn::Cidr',
+  findInMap: 'Fn::FindInMap',
+  getAzs: 'Fn::GetAZs',
+  if: 'Fn::If',
+  importValue: 'Fn::ImportValue',
+  join: 'Fn::Join',
+  select: 'Fn::Select',
+  split: 'Fn::Split',
+  sub: 'Fn::Sub',
+  transform: 'Fn::Transform',
+  and: 'Fn::And',
+  or: 'Fn::Or',
+  not: 'Fn::Not',
+  equals: 'Fn::Equals',
+  args: 'CDK::Args',
+  length: 'Fn::Length',
+  toJsonString: 'Fn::ToJsonString',
+};
+
 export function isExpression(x: unknown): x is TemplateExpression {
   try {
     assertExpression(x);
@@ -239,7 +266,7 @@ export function assertExpression(x: unknown): TemplateExpression {
     'intrinsic',
   ];
   if (!expressionTypes.includes(expressionType)) {
-    throw new ParserError(
+    throw new SyntaxError(
       `Expected ${expressionTypes.join('|')}, got: '${JSON.stringify(
         expressionType
       )}'`
@@ -249,25 +276,6 @@ export function assertExpression(x: unknown): TemplateExpression {
   return x as TemplateExpression;
 }
 
-export function unparseExpression(x: TemplateExpression): any {
-  switch (x.type) {
-    case 'string':
-    case 'number':
-    case 'boolean':
-      return x.value;
-    case 'null':
-      return null;
-    case 'array':
-      return x.array.map(unparseExpression);
-    case 'object':
-      return Object.fromEntries(
-        Object.entries(x.fields).map(([k, v]) => [k, unparseExpression(v)])
-      );
-    default:
-      return x;
-  }
-}
-
 export function parseExpression(x: unknown): TemplateExpression {
   if (x == null) {
     return { type: 'null' };
@@ -287,12 +295,12 @@ export function parseExpression(x: unknown): TemplateExpression {
   }
 
   const INTRINSIC_TABLE: Record<string, (x: unknown) => IntrinsicExpression> = {
-    Ref: (value) => ({
+    [INTRINSIC_NAME_MAP.ref]: (value) => ({
       type: 'intrinsic',
       fn: 'ref',
       logicalId: assertString(value),
     }),
-    'Fn::GetAtt': (value) => {
+    [INTRINSIC_NAME_MAP.getAtt]: (value) => {
       if (typeof value == 'string') {
         const [id, ...attrs] = value.split('.');
         value = [id, attrs.join('.')];
@@ -305,7 +313,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         attribute: parseExpression(xs[1]),
       };
     },
-    'CDK::GetProp': (value) => {
+    [INTRINSIC_NAME_MAP.getProp]: (value) => {
       if (typeof value == 'string') {
         const [id, ...attrs] = value.split('.');
         value = [id, attrs.join('.')];
@@ -318,12 +326,12 @@ export function parseExpression(x: unknown): TemplateExpression {
         property: assertString(xs[1]),
       };
     },
-    'Fn::Base64': (value) => ({
+    [INTRINSIC_NAME_MAP.base64]: (value) => ({
       type: 'intrinsic',
       fn: 'base64',
       expression: parseExpression(value),
     }),
-    'Fn::Cidr': (value) => {
+    [INTRINSIC_NAME_MAP.cidr]: (value) => {
       const xs = assertList(value, [2, 3]);
       return {
         type: 'intrinsic',
@@ -333,7 +341,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         netMask: xs[2] ? parseExpression(xs[2]) : undefined,
       };
     },
-    'Fn::FindInMap': (value) => {
+    [INTRINSIC_NAME_MAP.findInMap]: (value) => {
       const xs = assertList(value, [3]);
       return {
         type: 'intrinsic',
@@ -343,12 +351,12 @@ export function parseExpression(x: unknown): TemplateExpression {
         key2: parseExpression(xs[2]),
       };
     },
-    'Fn::GetAZs': (value) => ({
+    [INTRINSIC_NAME_MAP.getAzs]: (value) => ({
       type: 'intrinsic',
       fn: 'getAzs',
       region: parseExpression(value),
     }),
-    'Fn::If': (value) => {
+    [INTRINSIC_NAME_MAP.if]: (value) => {
       const xs = assertList(value, [3]);
       return {
         type: 'intrinsic',
@@ -358,12 +366,12 @@ export function parseExpression(x: unknown): TemplateExpression {
         else: parseExpression(xs[2]),
       };
     },
-    'Fn::ImportValue': (value) => ({
+    [INTRINSIC_NAME_MAP.importValue]: (value) => ({
       type: 'intrinsic',
       fn: 'importValue',
       export: parseExpression(value),
     }),
-    'Fn::Join': (value) => {
+    [INTRINSIC_NAME_MAP.join]: (value) => {
       const xs = assertList(value, [2]);
       return {
         type: 'intrinsic',
@@ -372,7 +380,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         list: parseListIntrinsic(xs[1]),
       };
     },
-    'Fn::Select': (value) => {
+    [INTRINSIC_NAME_MAP.select]: (value) => {
       const xs = assertList(value, [2]);
       return {
         type: 'intrinsic',
@@ -381,7 +389,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         objects: parseListIntrinsic(xs[1]),
       };
     },
-    'Fn::Split': (value) => {
+    [INTRINSIC_NAME_MAP.split]: (value) => {
       const xs = assertList(value, [2]);
       return {
         type: 'intrinsic',
@@ -390,7 +398,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         value: parseExpression(xs[1]),
       };
     },
-    'Fn::Sub': (value) => {
+    [INTRINSIC_NAME_MAP.sub]: (value) => {
       let pattern: string;
       let context: Record<string, TemplateExpression>;
       if (typeof value === 'string') {
@@ -412,7 +420,7 @@ export function parseExpression(x: unknown): TemplateExpression {
         additionalContext: context,
       };
     },
-    'Fn::Transform': (value) => {
+    [INTRINSIC_NAME_MAP.transform]: (value) => {
       const fields = assertObject(value);
 
       const parameters = parseObject(assertField(fields, 'Parameters'));
@@ -423,28 +431,28 @@ export function parseExpression(x: unknown): TemplateExpression {
         parameters,
       };
     },
-    'Fn::And': (value) => {
+    [INTRINSIC_NAME_MAP.and]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'and',
         operands: assertList(value).map(parseExpression),
       };
     },
-    'Fn::Or': (value) => {
+    [INTRINSIC_NAME_MAP.or]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'or',
         operands: assertList(value).map(parseExpression),
       };
     },
-    'Fn::Not': (value) => {
+    [INTRINSIC_NAME_MAP.not]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'not',
         operand: parseExpression(value),
       };
     },
-    'Fn::Equals': (value) => {
+    [INTRINSIC_NAME_MAP.equals]: (value) => {
       const [x1, x2] = assertList(value, [2]);
       return {
         type: 'intrinsic',
@@ -453,21 +461,21 @@ export function parseExpression(x: unknown): TemplateExpression {
         value2: parseExpression(x2),
       };
     },
-    'CDK::Args': (value) => {
+    [INTRINSIC_NAME_MAP.args]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'args',
         array: assertList(value).map(parseExpression),
       };
     },
-    'Fn::Length': (value) => {
+    [INTRINSIC_NAME_MAP.length]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'length',
         list: parseListIntrinsic(value),
       };
     },
-    'Fn::ToJsonString': (value) => {
+    [INTRINSIC_NAME_MAP.toJsonString]: (value) => {
       return {
         type: 'intrinsic',
         fn: 'toJsonString',
@@ -547,7 +555,7 @@ export function assertIntrinsic<T extends IntrinsicExpression['fn']>(
     }
   }
 
-  throw new ParserError(
+  throw new SyntaxError(
     `Expected one of Intrinsic Functions [${fns.join(
       '|'
     )}], got: ${JSON.stringify(x)}`
diff --git a/src/parser/template/overrides.ts b/src/parser/template/overrides.ts
index e892d2e..bf7f51a 100644
--- a/src/parser/template/overrides.ts
+++ b/src/parser/template/overrides.ts
@@ -5,7 +5,6 @@ import {
   assertObject,
   assertString,
   assertTrue,
-  ParserError,
 } from '../private/types';
 import { ifField, parseExpression, TemplateExpression } from './expression';
 
@@ -53,7 +52,7 @@ function parseOverride(x: unknown): ResourceOverride {
     case 'RemoveResource':
       return parseRemoveResource(override);
     default:
-      throw new ParserError('Unexpected Error');
+      throw new SyntaxError('Unexpected Error');
   }
 }
 
diff --git a/src/type-resolution/expression.ts b/src/type-resolution/expression.ts
index 0f7f73a..aa4b3fe 100644
--- a/src/type-resolution/expression.ts
+++ b/src/type-resolution/expression.ts
@@ -1,4 +1,5 @@
 import * as reflect from 'jsii-reflect';
+import { unparseExpression } from '../error-handling/unparse';
 import {
   ArrayExpression,
   BooleanLiteral,
@@ -8,7 +9,6 @@ import {
   ObjectExpression,
   StringLiteral,
   TemplateExpression,
-  unparseExpression,
 } from '../parser/template';
 import {
   InitializerExpression,
diff --git a/src/type-resolution/primitives.ts b/src/type-resolution/primitives.ts
index ef1b4d4..ed4abfd 100644
--- a/src/type-resolution/primitives.ts
+++ b/src/type-resolution/primitives.ts
@@ -1,5 +1,4 @@
 import * as reflect from 'jsii-reflect';
-import { ParserError } from '../parser/private/types';
 import { IntrinsicExpression, TemplateExpression } from '../parser/template';
 
 export interface AnyTemplateExpression {
@@ -28,7 +27,7 @@ export interface VoidExpression {
 
 export function assertVoid(x: unknown): void {
   if (x !== undefined || x !== null) {
-    throw new ParserError(`Expected nothing, got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected nothing, got: ${JSON.stringify(x)}`);
   }
 }
 
@@ -45,7 +44,7 @@ export function assertLiteralOrIntrinsic<
   type: T
 ): (TemplateExpression & { type: T }) | IntrinsicExpression {
   if (![type, 'intrinsic'].includes(x.type)) {
-    throw new ParserError(`Expected ${type}, got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected ${type}, got: ${JSON.stringify(x)}`);
   }
 
   return x as TemplateExpression & { type: T };
diff --git a/src/type-resolution/references.ts b/src/type-resolution/references.ts
index 5770a05..1be150e 100644
--- a/src/type-resolution/references.ts
+++ b/src/type-resolution/references.ts
@@ -1,4 +1,3 @@
-import { ParserError } from '../parser/private/types';
 import {
   GetPropIntrinsic,
   RefIntrinsic,
@@ -20,7 +19,7 @@ export function resolveRefToValue(x: RefIntrinsic): ResolveReferenceExpression {
 
 export function assertRef(x: TemplateExpression): RefIntrinsic {
   if (x.type !== 'intrinsic' || x.fn !== 'ref') {
-    throw new ParserError(`Expected Ref, got: ${JSON.stringify(x)}`);
+    throw new SyntaxError(`Expected Ref, got: ${JSON.stringify(x)}`);
   }
 
   return x;
diff --git a/src/type-resolution/union.ts b/src/type-resolution/union.ts
index 5b62984..6731d5e 100644
--- a/src/type-resolution/union.ts
+++ b/src/type-resolution/union.ts
@@ -1,5 +1,4 @@
 import * as reflect from 'jsii-reflect';
-import { ParserError } from '../parser/private/types';
 import { TemplateExpression } from '../parser/template/expression';
 import { TypedTemplateExpression } from './expression';
 import { resolveExpressionType } from './resolve';
@@ -8,12 +7,12 @@ export function resolveUnionOfTypesExpression(
   x: TemplateExpression,
   unionTypeRefs: reflect.TypeReference[]
 ): TypedTemplateExpression {
-  const errors = new Array<TypeError | ParserError>();
+  const errors = new Array<TypeError | SyntaxError>();
   for (const typeRef of unionTypeRefs) {
     try {
       return resolveExpressionType(x, typeRef);
     } catch (e) {
-      if (!(e instanceof TypeError || e instanceof ParserError)) {
+      if (!(e instanceof TypeError || e instanceof SyntaxError)) {
         throw e;
       }
       errors.push(e);


Test patch:
diff --git a/test/evaluate/errors.test.ts b/test/evaluate/errors.test.ts
index de724bf..016c7a1 100644
--- a/test/evaluate/errors.test.ts
+++ b/test/evaluate/errors.test.ts
@@ -1,4 +1,5 @@
 import { expect } from 'expect';
+import { DeclarativeStackError } from '../../src/error-handling';
 import { Template } from '../../src/parser/template';
 import { Testing } from '../util';
 
@@ -94,4 +95,56 @@ suite('Evaluation errors', () => {
       );
     });
   });
+
+  suite('Multiple errors', () => {
+    test('Evaluation errors are collected', async () => {
+      // GIVEN
+      const template = {
+        Resources: {
+          SiteDistribution: {
+            Type: 'aws-cdk-lib.aws_cloudfront.Distribution',
+            Properties: {
+              certificate: {
+                Ref: 'SiteBucket', // this should be SiteCertificate
+              },
+              defaultRootObject: 'index.html',
+              domainNames: [
+                {
+                  Ref: 'DomainName',
+                },
+              ],
+              minimumProtocolVersion: 'TLS_V1_2_2021',
+              defaultBehavior: {
+                origin: {
+                  'aws-cdk-lib.aws_cloudfront_origins.S3Origin': [
+                    { Ref: 'SiteCertificate' }, // this should be SiteBucket
+                    {
+                      originAccessIdentity: {
+                        Ref: 'CloudFrontOAI',
+                      },
+                    },
+                  ],
+                },
+              },
+            },
+          },
+        },
+      };
+      const synth = Testing.synth(await Template.fromObject(template), {
+        validateTemplate: false,
+      });
+
+      // THEN
+      await expect(synth).rejects.toThrow(DeclarativeStackError);
+      try {
+        await synth;
+      } catch (error) {
+        const msg = error.toString();
+        expect(msg).toContain('No such Resource or Parameter: SiteCertificate');
+        expect(msg).toContain('No such Resource or Parameter: CloudFrontOAI');
+        expect(msg).toContain('No such Resource or Parameter: SiteBucket');
+        expect(msg).toContain('No such Resource or Parameter: DomainName');
+      }
+    });
+  });
 });