langium
langium copied to clipboard
Add a special syntax for binary operators
Motivation is in the discussion #751.
The proposal is to add the ability to define binary operations shorter with the following syntax:
@binary_op (left_operand=left, right_operand=right, operator=op)
BinaryExpression infers Expression:
, infixl ("*" | "/") Primary
, infixl ('+' | '-') Primary
, infixl "&&" Primary
, infixl "||" Primary
, infixn ".." Primary // range operator; no associativity
, infixr '=' Primary
;
-
@binary_op
-- an annotation to defined a rule with the special syntax -
left_operand
,right_operand
,operator
-- arguments of the annotation that reflect property names in the generated interfaceBinaryExpression
- arguments can be omitted -- then default names can be used:
left_operand
,right_operand
,operator
- the rule with the special annotation
@binary_op
consists of comma-separated operator declarations; the order of these declarations reflects the operator precedence - each operator declaration consists of 3 parts:
- associativity keyword
infix(r|l|n)
- operators -- usual syntax with pipes
- the name of the rule responsible for parsing the primary expression
- associativity keyword
The syntax above must be semantically equivalent to the current syntax:
Expression:
Assignment;
Assignment infers Expression:
Range ({infer BinaryExpression.left=current} operator='=' right=Assignment)?;
Range infers Expression:
Or ({infer BinaryExpression.left=current} operator='..' right=Or)?;
Or infers Expression:
And ({infer BinaryExpression.left=current} operator='||' right=And)*;
And infers Expression:
Addition ({infer BinaryExpression.left=current} operator='&&' right=Addition)*;
Addition infers Expression:
Multiplication ({infer BinaryExpression.left=current} operator=('+' | '-') right=Multiplication)*;
Multiplication infers Expression:
Primary ({infer BinaryExpression.left=current} operator=('*' | '/') right=Primary)*;
Unary operators must be defined using the current syntax, for example:
Primary infers Expression:
'(' Expression ')'
| {infer UnaryPrefix} op=('+' | '-') value=Expression
| {infer NumberLiteral} value=NUMBER
;
Both syntaxes have to generate the same types for the ast.ts
:
export type Expression = BinaryExpression | NumberLiteral | UnaryPrefix;
export interface BinaryExpression extends AstNode {
left: Expression
operator: '&&' | '*' | '+' | '-' | '..' | '/' | '=' | '||'
right: Expression
}
export interface UnaryPrefix extends AstNode {
op: '+' | '-'
value: Expression
}
export interface NumberLiteral extends AstNode {
value: number
}
Cool, that you have done something. For me this topic is a hot potato that I would avoid to touch, since it is very subjective. There are already some solutions out there with different syntaxes.
What I like:
- all operators are closed in one kind of rule. Good because you are able to define operators on different concepts, like on value and type level for example
- the idea about naming parts of the rule, like left, right, op. This could be tricky (example NameProvider) if you have multiple levels of expressions, like said in the first point
- the syntax of writing a table, because even if it is not enforced by syntax, it would be a great help for the user to read this information
What could be better:
- we have no annotations yet, I think we should be careful here not to introduce something that opens the longings of others to have it somewhere else as well
- the „infix“ mentioned there is not used, it is always infix, so you could remove it (postfix {1 2 +} and prefix {+ 1 2} could be used here for example, but I cannot imagine that someone would need it for binary operations)
- as mentioned in Slack, you need to decide between left or right associativity. Saying no should default to left.
Other proposal:
Expression binaryOperators Primary: // same schema: Rule Verb Rule
{left} (‚+‘|‘-‚) //recycles actions, terminals and alternatives
| {left} ‚*‘ // order of alternatives defines the priority
| {right} ‚^‘
…
;
Instead of the equal alternative operator | we could use a ordered alternative operator like |>…
Expression …: {right} ‚^‘ |> {left} ‚*‘ |> {left} ‚+‘;
Instead of Expression binaryOperators Primary:
I first thought of a template expansion like if you would call a template library function… ˋExpression expands BinaryOperationTemplate(Primary)ˋ, but that would introduce a new concept of templates, with which we should be careful as well.
I agree with @Lotes here. I actually had a very similar syntax in mind:
// Using `|` for normal alternatives, `,` for splitting operators
operator BinaryExpression on Primary:
'+' | '-', '*' | '/', '^', '..', '=';
// Use `<` and `>` for associativity
operator BinaryExpression on Primary:
<= '+' | '-', // left associative by default
<= '*' | '/',
=> '^', // right associative
<> '..', // no associativity
=> '='; // right associative again
To explain my reasoning here a bit: Using {right}
and {left}
for associativity is ambiguous with the action syntax. I would use a rather lightweight solution such as <
and >
. Even though I prefer |>
over ,
for differentiating between operator precedence. However, that is difficult to read with <
and >
in there.
I would also remove the infix information, since this style of operator usage is only useful in case you have an infix notation. Prefix notation can easily be parsed in a different way (think Addition: '+' Primary Primary
), while nobody uses postfix notations.
Add a rename operator as code actions {§left=“lhs”} {§right=“rhs”} {§operator=“op”}
I used §
because it states "what is the law" for generator and other parts of Langium :D