Skip to content

Commit

Permalink
Update spec to split out partials and "macros"
Browse files Browse the repository at this point in the history
Summary:
Partial applications today have some glaring problems:
1. Must be in a new file even if they are used once.
2. Assume the context of the call-site, making it impossible to discern what the *inputs* of a partial are supposed to be.

Partial blocks (as currently specified) were introduced to address these issues but it was never implemented (as it was under specified and its value was unclear at the time). **This diff attempts to specify the feature such that it can actually be implemented** (later in the stack).

 ---

## 1. Current `partial` are renamed to `macro`. Why?

The main reason to split out the names of existing partial applications (bad) and what is proposed here (better) is simple: **they are very different semantically and sharing a name makes it very confusing to discuss**.

I've decided that `partial` (what most people are familiar with) should be the "good" feature and make use of the `partial` keyword in the language. The older "bad" feature gets the name `macro` because:
- It somewhat resembles macros from the C preprocessor, especially its lack of input / output hygiene.

One other big difference is that (as of the current proposal), `partial`s are used *within one file* while macros are *across files*. This can change in the future, but it's another reason to give them different names.

 ---

## 2. Why `{{#partial}}` instead of `{{> }}` like what we have already?

1. `macro` names are file names (`path-component`) which is *not* an `identifier` like the rest of Whisker. This is for legacy reasons and ideally we should try to move away from it.
2. `{{#partial}}` is more explicit and matches the definition of *statement* that we have today.
3. In the current dichotomy of `path-component` vs. `identifier`, it would be a mess to specify named arguments syntax that we want. **Notably, `path-component` and `identifier` are ambiguous with each other.**

 ---

## 3. Why `{{#let partial}}` and `{{#partial}}`?

`{{#let}}` is an existing mechanism for binding names in Whisker so `{{#let partial}}` seems like a natural extension. Note that `partial` is a keyword (not allowed an identifier) so it does not conflict with existing syntax.

`{{#let partial}}` and `{{#partial}}` clearly show that these are coupled language features.

 ---

## 4. Why are `{{#let partial}}` blocks limited to be at the top of files?

This limitation makes name resolution *much* *much* simpler. One of the patterns we're trying to solve is the use of recursive and mutually recursive partials (I've noticed their use in C++, Python, and JSON codegen already). This means that partials might need to refer to names that are defined *after* it.

```
{{#let partial mutual-1 as |...|}}
  ...
  {{#partial mutual-2 ...}}
{{/let partial}}

{{#let partial mutual-2 as |...|}}
  ...
  {{#partial mutual-1 ...}}
{{/let partial}}
```
By keeping everything in one scope (the top of the file), we don't have to specify complicated name resolution rules etc. Every file "just" has a simple `map<identifier, partial>` and that's it!

We can relax this restriction in the future if needed. For now I think this solves the problems we need it for.

 ---

Reviewed By: rmakheja

Differential Revision: D68424875

fbshipit-source-id: 686a7fb523fb5408e784ab358ad1e50e5374deeb
  • Loading branch information
praihan authored and facebook-github-bot committed Jan 22, 2025
1 parent 72d0a1c commit e384726
Showing 1 changed file with 91 additions and 85 deletions.
176 changes: 91 additions & 85 deletions third-party/thrift/src/thrift/doc/contributions/whisker.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,17 @@ There are four categories of `template`s:
* All blocks contain zero or more body elements inside them.
* ***Statements***`{{#foo}}` — non-rendering operations like name bindings ([`{{#let}}`](#let-statements)), `{{#else}}` etc.
* All statements are of the form `{{# ...}}`. Unlike blocks, there is no closing tag.
* [***Partial Application***](#partial-applications)`{{> foo}}` — for reusable templates.
* [***Macros***](#macros)`{{> foo}}` — for reusable templates.

<Grammar>

```
template → { interpolation | block | statement | partial-apply }
template → { interpolation | block | statement | macro }
interpolation → { <see below> }
block → { <see below> }
statement → { <see below> }
partial-apply → { <see below> }
macro → { <see below> }
```

</Grammar>
Expand Down Expand Up @@ -622,88 +622,36 @@ pragma-statement → { "{{" ~ "#" ~ "pragma" ~ ( "single-line" ) ~ "}}" }

</Grammar>

### Partial Blocks
### Partial Blocks & Statements

:::warning
`{{#partial}}` blocks have not been implemented yet.
`{{#let partial}}` blocks and `{{#partial}}` statements have not been implemented yet.
:::

Partial blocks allow defining reusable templates within a Whisker template. A simple example of a `{{#partial}}` block might be:
Partial blocks allow defining reusable templates within a Whisker template. They are not rendered unless *applied* (by name). A simple example of a `{{#let partial}}` block might be:

```handlebars
{{#partial greeting as |person|}}
{{#let partial greeting as |person|}}
Greetings, {{person.firstName}} {{person.lastName}}!
{{/partial}}
{{! example partial application }}
{{> greeting person=person}}
```

Partial blocks must be applied with [partial applications](#partial-applications). See below.

<Grammar>

{{/let partial}}
```
partial-block → { partial-block-open ~ body* ~ partial-block-close }
partial-block-open → { "{{#" ~ "partial" ~ path-component ~ routine-capture? ~ "}}" }
partial-block-capture → { "as" ~ "|" ~ identifier+ ~ "|" }
partial-block-close → { "{{/" ~ "partial" ~ "}}" }
path-component → { <see below> }
```

</Grammar>

Whisker `{{#partial}}` blocks are based on [Handlebars partial parameters](https://handlebarsjs.com/guide/partials.html#partial-parameters).

### Partial Applications

Partials are reusable templates that are not rendered unless *applied* (by name). A simple example of partial application might be:
Partial blocks must be applied with `{{#partial ...}}` statements. A simple example for a `{{#partial}}` statement for the above block might be:

```handlebars
{{> path/to/my-partial}}
```

:::note
Currently, partials cannot be defined in Whisker — they must be provided by the runtime environment (e.g. C++).
This will change once [`{{#partial}}` blocks](#partial-blocks) are implemented.
:::

Partial applications (without captures) assume the [scope](#scopes) at the site of application. This behavior is analogous to [C preprocessor macro expansion](https://en.wikipedia.org/wiki/C_preprocessor#Macro_definition_and_expansion). Names accessible from the site of the application are also available within the block.

<Example title="Example with implied context (macro)">

```handlebars title=example.whisker
{{#partial greeting as |person|}}
Greetings, {{person.firstName}} {{person.lastName}}!
{{/partial}}
{{> greeting}}
```

```json title=Context
{
"person": {
"firstName": "Dave",
"lastName": "Grohl"
}
}
{{#partial greeting person=person}}
```

```text title=Output
Greetings, Dave Grohl!
```
The `{{#partial}}` statement must include named arguments that are [bound](#scopes) to `expression`s, matching the captures (`as |...|`) from the definition.

</Example>

[Partial applications (with captures)](#partial-blocks) have names (provided via `as`) [bound](#scopes) to `expression`s provided during application. The contained body is rendered with a [derived evaluation context](#derived-evaluation-context). Names accessible from the site of the application are **not** *implicitly* available within the block.
The contained body of the `{{#let partial}}` block is rendered with a [derived evaluation context](#derived-evaluation-context). Names accessible from the site of the application are **not** *implicitly* available within the block.

<Example>

```handlebars
{{#partial greeting as |person|}}
{{#let partial greeting as |person|}}
Greetings, {{person.firstName}} {{person.lastName}}!
{{/partial}}
{{/let partial}}
{{> greeting person=dave}}
```
Expand All @@ -723,22 +671,21 @@ Greetings, Dave Grohl!

</Example>

Partial applications retain the *preceding indentation* at the site of the application. Every line of the partial being applied is indented by this same amount.
Partial statements retain the *preceding indentation* at the site of the application. Every line of the partial being applied is indented by this same amount.

<Example title="Example with indentation">

```handlebars title=example.whisker
{{#let partial president as |person|}}
{{person.lastName}}
{{person.firstName}}
{{/let partial}}
Some historic presidents are:
{{#each presidents as person}}
{{> common/president}}
{{#partial president person=person}}
{{/each}}
```

```handlebars title=common/president.whisker
{{person.lastName}}
{{person.firstName}}
```

```json title=Context
{
"presidents": [
Expand Down Expand Up @@ -768,9 +715,68 @@ Some historic presidents are:
<Grammar>

```
partial-apply → { "{{" ~ ">" ~ partial-lookup ~ partial-argument* ~ "}}" }
partial-lookup → { path-component ~ ("/" ~ path-component)* }
partial-argument → { identifier ~ "=" ~ expression }
partial-block → { partial-block-open ~ body* ~ partial-block-close }
partial-block-open → { "{{#" ~ "let" ~ "partial" ~ identifier ~ partial-block-capture ~ "}}" }
partial-block-capture → { "as" ~ "|" ~ identifier+ ~ "|" }
partial-block-close → { "{{/" ~ "let" ~ "partial" ~ "}}" }
partial-statement → { "{{" ~ "#" ~ "partial" ~ identifier ~ partial-argument+ ~ "}}" }
partial-argument → { identifier ~ "=" ~ expression }
```

</Grammar>

:::note
Partial blocks may only appear at the top of a source file.
:::

Whisker `{{#let partial}}` blocks are based on [Handlebars partial parameters](https://handlebarsjs.com/guide/partials.html#partial-parameters).

### Macros

Macros are reusable templates that are not rendered unless *applied* (by a path). A simple example of macro application might be:

```handlebars
{{> path/to/my-partial}}
```

:::note
Macros cannot be defined in Whisker — they must be provided by the runtime environment (e.g. C++).
:::

Macros assume the [scope](#scopes) at the site of application. This behavior is analogous to [C preprocessor macro expansion](https://en.wikipedia.org/wiki/C_preprocessor#Macro_definition_and_expansion).
Names accessible from the site of the application are also available within the block.

<Example>

```handlebars title=example.whisker
{{> greeting}}
```

```handlebars title=greeting.whisker
Greetings, {{person.firstName}} {{person.lastName}}!
```

```json title=Context
{
"person": {
"firstName": "Dave",
"lastName": "Grohl"
}
}
```

```text title=Output
Greetings, Dave Grohl!
```

</Example>

<Grammar>

```
macro → { "{{" ~ ">" ~ macro-lookup ~ "}}" }
macro-lookup → { path-component ~ ("/" ~ path-component)* }
path-component → { <see below> }
```
Expand Down Expand Up @@ -881,13 +887,13 @@ When resolving an *identifier* like `name`, the process works as follows:
4. Move to the previous scope in the stack and repeat (2) and (3).
* If the bottom of the stack is reached without resolution, return an error.

Local bindings can be added to the current scope using [`{{#let}}`](#let-statements), `as` captures in [`{{#each}}`](#each-blocks) or [`{{#partial}}`](#partial-blocks), and similar constructs.
Local bindings can be added to the current scope using [`{{#let}}`](#let-statements), `as` captures in [`{{#each}}`](#each-blocks), and similar constructs.

Certain scopes lack an **implicit context** `object`, which is represented by `null`. For instance, [`{{#if}}`](#if-blocks) blocks always have a `null` context. [`{{#each}}`](#each-blocks) blocks have a `null` context when captures are present.

### Derived Evaluation Context

`{{#partial}}` blocks with `as` captures are rendered within a new evaluation context *derived* from the the call site. This context starts with an empty stack but retains access to the same global scope `map`.
`{{#let partial}}` blocks are rendered within a new evaluation context *derived* from the the call site. This context starts with an empty stack but retains access to the same global scope `map`.

## Standalone Tags

Expand All @@ -896,9 +902,9 @@ The Mustache spec defines rules around a concept called [*standalone lines*](htt
If a line has control flow tags, but is otherwise only whitespace, then implementations should strip the entire line from the output.
The following tags are standalone-stripping eligible:
* `{{! ... }}`[comments](#comments)
* `{{# ... }}` — blocks ([`{{#if}}`](#if-blocks), [`{{#each}}`](#each-blocks), [`{{#with}}`](#with-blocks)) and statements ([`{{#let}}`](#let-statements))
* `{{# ... }}` — blocks ([`{{#if}}`](#if-blocks), [`{{#each}}`](#each-blocks), [`{{#with}}`](#with-blocks), [`{{#let partial}}`](#partial-blocks--statements)) and statements ([`{{#let}}`](#let-statements), [`{{#partial}}`](#partial-blocks--statements))
* `{{/ ... }}` — closing tag for blocks listed above
* `{{> ... }}`[partial applications](#partial-applications)
* `{{> ... }}`[macros](#macros)

<Example>

Expand Down Expand Up @@ -985,10 +991,10 @@ Notice that `boolean` and `.condition` are on separate lines, yet both lines wer

</Example>

[Partial applications](#partial-applications) have special behavior in the context of standalone line stripping, even though they perform interpolation.
If a partial application is standalone, then the whitespace **to the left is preserved**, while the one **to the right is stripped**.
[Partial statements](#partial-blocks--statements) and [macros](#macros) have special behavior in the context of standalone line stripping, even though they perform interpolation.
If the application is standalone, then the whitespace **to the left is preserved**, while the one **to the right is stripped**.

<Example title="Example with partial application">
<Example title="Example with macro">

```handlebars title=example.whisker
| *
Expand Down Expand Up @@ -1016,7 +1022,7 @@ hello world

</Example>

A standalone-stripped line can have multiple tags, as long as none of tags are [partial applications](#partial-applications).
A standalone-stripped line can have multiple tags, as long as none of tags are [partial statements](#partial-blocks--statements) or [macros](#macros).

<Example title="Example with multiple tags">

Expand Down Expand Up @@ -1044,7 +1050,7 @@ A standalone-stripped line can have multiple tags, as long as none of tags are [

</Example>

<Example title="Example with multiple tags and partial application">
<Example title="Example with multiple tags and macro">

```handlebars title=example.whisker
| *
Expand Down

0 comments on commit e384726

Please sign in to comment.