Layout Sensitivity¶
SDF3 supports definition of layout sensitive syntax by means of low-level layout constraints and high-level layout declarations.
Note
If you want to use layout constraints or layout declarations, you should specify the jsglr-version: layout-sensitive
parameter for SDF3, see configuration.
Layout Constraints¶
While we haven't covered layout constraints in this documentation, the paper of Erdweg et al.1 describes the concepts.
Layout Declarations¶
In the paper of Erdweg et al.1, the authors describe layout constraints in terms of restrictions involving the position of the subtree involved in the constraint (0
, 1
, ...), token selectors (first
, left
, last
and right
), and position selectors as lines and columns (line
and col
).
This mechanism allows writing layout constraints to express alignment, offside and indentation rules, but writing such constraints is rather cumbersome and error prone. Alternatively, one may write layout constraints using layout declarations, which are more declarative specifications and abstract over lines, columns and token selectors as the original layout constraints from the Erdweg et al. paper1.
Tree selectors¶
To specify which trees should be subject to a layout constraint, one may use: tree positions, SDF3 labeled non-terminals, or unique literals that occurs in the production. For example:
context-free syntax
Stmt.IfElse = "if" Exp "then" Stmts "else" else:Stmts {layout(
indent "if" 3, else &&
align 3 else &&
align "if" "else"
)}
In the layout constraint for the production above, else
refers to the tree for the labeled non-terminal else:Stmts
, "if"
refers to the tree
corresponding to the "if"
literal and the number 3 correspond to the tree at position 3 in the parse tree (starting at 0, ignoring trees for LAYOUT?
).
align
¶
The layout constraint layout(align x y1, ..., yn)
specifies that the trees indicated by the tree selectors yi
should be aligned with the tree indicated by the tree selector x
, i.e., all these trees should start in the same column.
For example, if we consider the production above, the following program is correct according to the align
constraints:
if x < 0 then
··x = 0
else
··y = 1
Whereas, the following program is incorrect because neither the if and else keyword align (align "if" "else"
), nor the statements in the branches (align 3 else
):
if x < 0 then
··x = 0
·else
···y = 1
align-list
¶
The constraint align-list
can be used to indicate that all subtrees within a list should be aligned.
That is, a constraint layout(align-list x)
, where x
is a tree selector for a list subtree, can be used to enforce such constraint.
For example, consider the following production and its layout constraint:
context-free syntax
Stmt.If = "if" Exp "then" then:Stmt* {layout(
align-list then
)}
This constraint indicates that statements inside the list should be aligned. Therefore, the following program is correct according to this constraint:
if x < 0 then
··x = 0
··y = 4
··z = 2
And the following program is invalid, as the second statement is misaligned:
if x < 0 then
··x = 0
···y = 4
··z = 2
offside
¶
The offside rule is very common in layout-sensitive languages. It states that all lines after the first one should be further to the right compared to the first line. For a description of how the offside rule can be modelled with layout constraints, refer to Erdweg et al.1. An example of a declarative specification of the offside rule can be seen in the production below:
context-free syntax
Stmt.Assign = <<ID> = <Exp>> {layout(offside 3)}
The layout constraint specifies that when the expression in the statement spams multiple lines, all following lines should be indented with respect to the column where the expression started. For example, the following program is valid according to this constraint:
x = 4 * 10
·····+ 2
However, the following program is not valid, as the second line of the expression starts at the same column as the first line:
x = 4 * 10
····+ 2
Note that if the expression is written on a single line, the constraint is also verified. That is, the following program successfully parses:
x = 4 * 10 + 2
It is also possible to use the offside relation on different trees. For example, consider the constraint in the following production:
context-free syntax
Stmt.If = "if" Exp "then" then:Stmt* {layout(
offside "if" then
)}
This constraint states that all lines (except the first) of the statements in the then
branch should be indented with respect to the if
literal.
Thus, the following program is invalid according to this layout constraint, because the statement x = 2
should be indented with relation to the topmost if
.
if x < 0 then
··if y < 0 then
x = 2
In general, an offside
constraint involving more than a single tree is combined with indent
constraint to enforce that the column of the first and all subsequent lines should be indented.
indent
¶
An indent constraint indicates that the column of the first line of a certain tree should be further to the right with respect to another tree. For example, consider the following production:
context-free syntax
Stmt.If = "if" Exp "then" then:Stmt* {layout(
indent "if" then
)}
This constraint indicates that the first line of the list of statements should be indented with respect to the if
literal.
Thus, according to this constraint the following program is valid:
if x < 0 then
··x = 2
Note that if the list of statements in the then branch spams multiple lines, the constraint does not apply to its subsequent lines. For example, consider the following program:
if x < 0 then
··x = 2 + 10
* 4
y = 3
This program is still valid, since the column of the first line of the first assignment is indented with respect to the if literal. To indicate that the first and all subsequent lines should be indented, an offside constraint should also be included.
context-free syntax
Stmt.If = "if" Exp "then" then:Stmt* {layout(
indent "if" then &&
offside "if" then
)}
With this constraint, the remainder of the expression * 4
should also be further to the right compared to the "if" literal.
The following program is correct according to these two constraints, since the second line of the first assignment and the second assignment are also indented with respect to the if
literal:
if x < 0 then
··x = 2 + 10
·* 4
·y = 3
newline-indent
¶
The newline-indent constraint indicates that a certain tree should both be located on a new line, as well as further to the right with respect to another tree.
For example, consider the following production:
context-free syntax
Exp.If = "if" Exp "then" then:Exp "else" Exp {layout(
newline-indent "if" then
)}
This constraint indicates that the "then" branch of the if-then-else expression needs to be on a new line and indented with respect to the "if" keyword. Thus, according to this constraint the following program is valid:
if x < 0 then
··x + 2 else x * 2
The newline-indent constraint does not require that the expression is located on the immediate next line, but rather that it is located below the reference tree. Consequently, the following is also allowed:
if x < 0 then
··x + 2
else x * 2
Note that the newline-indent constraint is relative to the first character in the reference tree. That means that, given the following syntax:
context-free syntax
Example.Example = a:FooBar b:Baz {layout(newline-indent a b)}
FooBar.FooBar = <foo bar>
Baz.Baz = <baz>
The following syntax is valid, despite bar
being indented further than baz
:
foo
····bar
··baz
single-line
¶
The single-line constraints indicates that the entirety of the given subtrees must be located on the same line. For example, consider the following production:
context-free syntax
Exp.If = "if" Exp ":" then:Exp "else" Exp {layout(
single-line "if" ":"
)}
This enforces that both the if
and :
tokens need to be on the same line. As a result, the condition expression also needs to be contained on the same line. Thus, the following program is valid for the given constraints:
if x < 3:
x + 2 else x * 2
Note that the entirety of the referenced tree needs to be on the same line. Consider the following syntax:
context-free syntax
Example.Example = a:Baz b:FooBar {layout(single-line a b)}
FooBar.FooBar = <foo bar>
Baz.Baz = <baz>
With this definition, the following program is invalid, despite baz
and the start of foo bar
being on the same line:
baz foo
bar
Using the single-line
constraint without any parameters will add a constraint that the entire production needs to be on a single line. This can be useful as a shorthand when your grammar requires that the entirety of a production is on the same line:
context-free syntax
Exp.Add = <<Exp> + <Exp>> {layout(single-line)}
// is equivalent to
Exp.Add = <<a:Exp> + <b:Exp>> {layout(single-line a b)}
Finally, all these layout declarations can be ignored by the parser and used only when generating the pretty-printer.
To do that, prefix the constraint with pp-
writing, for example, pp-offside
or pp-align
.
-
Sebastian Erdweg, Tillmann Rendel, Christian Kästner, and Klaus Ostermann. Layout-sensitive generalized parsing. In Krzysztof Czarnecki and Görel Hedin, editors, Software Language Engineering, 5th International Conference, SLE 2012, Dresden, Germany, September 26-28, 2012, Revised Selected Papers, volume 7745 of Lecture Notes in Computer Science, 244–263. Springer, 2012. URL: http://dx.doi.org/10.1007/978-3-642-36089-3_14, doi:10.1007/978-3-642-36089-3_14. ↩↩↩↩
Created: October 17, 2024