Skip to content

Commit

Permalink
Lay groundwork for lexical scope & improve errors
Browse files Browse the repository at this point in the history
First, this PR lays the groundwork for Ember RFC #432, which allows
components, helpers and modifiers to be passed around as first-class
values.

Second, it improves the compiler infrastructure to enable errors to be
reported (rather than thrown). Reported errors can now include a source
location to aid in reporting the error to the user.

*First-class components, helpers, and modifiers*

Currently, helpers and modifiers have the syntactic form `{{some-helper
some parameters}}`, and `some-helper` is required to be a single literal
name.

That name is then resolved (globally) into a helper by the environment,
and the compiler emits a call to that resolved helper into the compiled
program. Notably, the helper must be fully resolved at compile-time, and
the following does not work as expected:

```
{{#let x as |hello|}}
  {{hello world}}
{{/let}}
```

Glimmer does not currently have any way to construct a first-class
helper, so this gap is not particularly observable.

On the other hand:

```
{{#let (component "my-tab") as |tab|}}
  {{tab some=args}}
{{/let}}
```

This *does* work in Ember, but before this commit, it was not directly
supported in Glimmer. Instead, Ember does an AST transformation on the
above syntax to turn `{{tab some=args}}` in this situation into
`{{component tab some=args}}`.

The same problems exist in Glimmer VM for modifiers, which also have no
first-class representation yet.

All of these problems are addressed by changing the wire format (the
compilation IR) in Glimmer so that these positions are now just
expressions rather strings.

Additionally, this commit changes a number of disparate wire format
representations into a single "free variable" representation. A free
variable is a variable reference that refers to a name that is not
declared in the current scope.

After this PR, the name `hello` is a free variable in all of the
following contexts:

- `{{hello}}`
- `{{hello.world}}`
- `{{hello world}}`
- `{{helper hello}}`
- `{{#hello}}{{/hello}}`
- `<hello />`

To maintain compatibility, free variables in the wire format include a
context. For example in `{{hello world}}`, the `hello` is a free
variable whose context is a helper call, which means that the compiler
will try to find a helper named `hello`, and otherwise produce an error.

In `{{hello}}`, the context is `AppendSingleId`. That means that the
compiler will try to find a helper named `hello`, and if it fails to
find one, it will fall back to `{{this.hello}}`.

These behaviors are present in compatibility mode, which is the only
mode currently implemented. In strict mode (described in in-progress RFC
496), all helpers, components and modifiers must be imported, so free
variables have no semantics at all.

Finally, while this PR lays the groundwork for RFC 432, it does not
implement contextual modifiers or helpers, nor does it implement the
necessary compilation work to invoke a contextual helper. However, that
work is relatively straight forward now that the groundwork is laid.

*Better Errors*

In addition, this PR introduces improved error reporting.

Previously, the template compiler and opcode compiler threw exceptions
if they encountered compilation problems.

Now, the compilation stages can produce errors alongside the compilation
output, which can be reported to users.

For example, if the opcode compiler fails to resolve a helper, it emits
an `undefined` value instead of a helper invocation, and records the
error. The entire compilation process produces a runnable program as
well as a list of encountered errors.

In addition to improving error reporting, this PR also allows errors to
include a source location, and plumbs the source locations from the
parser all the way through to the opcode compiler.

This PR makes a first use of that infrastructure by reporting the
source location of a helper that could not be resolved when an error
occurred. It does not fully eliminate all thrown exceptions.

Next steps would include reporting the source location of unresolved
modifiers and components, as well as beefing up existing error cases to
go through the reporting infrastructure and have source locations.

Once that work is done, we should plumb the locations further, so that
opcodes could associate failures with source locations, and eventually
associating references themselves with their original source locations.
To accomplish those goals, we will need a good strategy for avoiding
overhead in production.
  • Loading branch information
wycats authored and tomdale committed Oct 18, 2019
1 parent 0b70ad2 commit 4c56c21
Show file tree
Hide file tree
Showing 95 changed files with 4,970 additions and 931 deletions.
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// Place your settings in this file to overwrite default and user settings.
{
"search.exclude": {
"**/node_modules/**": true,
"tmp": true
},
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules/**": true,
"**/.git": true,
"**/dist/**": true,
"dist": true,
"tmp/**": true
},
"files.watcherExclude": {
"**/.git/objects/**": true,
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true,
"tmp": true,
"dist": true
},
Expand Down
11 changes: 10 additions & 1 deletion build/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,18 @@ write(

let debugMetadata = strip`
import { MachineOp, Op, Option } from '@glimmer/interfaces';
import { fillNulls } from '@glimmer/util';
import { NormalizedMetadata } from '@glimmer/debug';
function fillNulls<T>(count: number): T[] {
let arr = new Array(count);
for (let i = 0; i < count; i++) {
arr[i] = null;
}
return arr;
}
export function opcodeMetadata(op: MachineOp | Op, isMachine: 0 | 1): Option<NormalizedMetadata> {
let value = isMachine ? MACHINE_METADATA[op] : METADATA[op];
Expand Down
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
"link:local": "node bin/yarn-link-local"
},
"resolutions": {
"typescript": "3.3.3",
"broccoli-typescript-transpiler/typescript": "3.3.3"
"typescript": "3.5.3",
"amd-name-resolver": "https://github.com/ember-cli/amd-name-resolver.git",
"broccoli-typescript-compiler": "http:https://github.com/wycats/broccoli-typescript-compiler.git#upgrade-typescript",
"broccoli-typescript-compiler/typescript": "3.5.3"
},
"dependencies": {
"@simple-dom/document": "^1.4.0",
Expand Down Expand Up @@ -63,7 +65,7 @@
"broccoli-persistent-filter": "^2.1.1",
"broccoli-rollup": "^2.0.0",
"broccoli-source": "^1.1.0",
"broccoli-typescript-compiler": "^4.1.0",
"broccoli-typescript-compiler": "http:https://github.com/wycats/broccoli-typescript-compiler.git#upgrade-typescript",
"dag-map": "^2.0.2",
"ember-cli": "~3.6.1",
"ember-cli-release": "^1.0.0-beta.2",
Expand Down Expand Up @@ -94,5 +96,9 @@
"documentation": ":memo: Documentation",
"internal": ":house: Internal"
}
},
"volta": {
"node": "10.16.3",
"yarn": "1.17.3"
}
}
}
15 changes: 12 additions & 3 deletions packages/@glimmer/bundle-compiler/lib/bundle-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
CompileTimeConstants,
Macros,
Option,
EncoderError,
HandleResult,
} from '@glimmer/interfaces';
import { compileStd, compilable, MacrosImpl } from '@glimmer/opcode-compiler';

Expand Down Expand Up @@ -205,7 +207,7 @@ export default class BundleCompiler {
preprocess(locator: ModuleLocator, input: string): SerializedTemplateBlock {
let options = { meta: locator, plugins: { ast: this.plugins } };
let ast = preprocess(input, options);
let template = TemplateCompiler.compile(ast);
let template = TemplateCompiler.compile(ast, input);
return template.toJSON();
}

Expand All @@ -217,11 +219,14 @@ export default class BundleCompiler {
* Performs the actual compilation of the template identified by the passed
* locator into the Program. ModuleLocatoreturns the VM handle for the compiled template.
*/
protected compileTemplate(locator: ModuleLocator): number {
protected compileTemplate(locator: ModuleLocator): HandleResult {
// If this locator already has an assigned VM handle, it means we've already
// compiled it. We need to skip compiling it again and just return the same
// VM handle.
let vmHandle = this.compilerModuleLocatorResolver().getHandleByLocator(locator);
let vmHandle:
| number
| { handle: number; errors: EncoderError[] }
| undefined = this.compilerModuleLocatorResolver().getHandleByLocator(locator);
if (vmHandle !== undefined) return vmHandle;

// It's an error to try to compile a template that wasn't first added to the
Expand All @@ -235,6 +240,10 @@ export default class BundleCompiler {
// handle (the address of the compiled program in the heap).
vmHandle = compilableTemplate.compile(syntaxCompilationContext(this.context, this.macros));

if (typeof vmHandle !== 'number') {
return vmHandle;
}

// Index the locator by VM handle and vice versa for easy lookups.
this.compilerModuleLocatorResolver().setHandleByLocator(locator, vmHandle);

Expand Down
14 changes: 13 additions & 1 deletion packages/@glimmer/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
export { defaultId, precompile, PrecompileOptions } from './lib/compiler';

export {
ProgramSymbols,
buildStatement,
buildStatements,
s,
c,
unicode,
NEWLINE,
} from './lib/builder';
export { BuilderStatement, Builder } from './lib/builder-interface';
export { default as TemplateCompiler } from './lib/template-compiler';

// exported only for tests
export { default as TemplateVisitor } from './lib/template-visitor';
export { default as WireFormatDebugger } from './lib/wire-format-debug';

export * from './lib/location';
Loading

0 comments on commit 4c56c21

Please sign in to comment.