Skip to content

Commit

Permalink
detect qualified ops as single-token lookahead in the scanner
Browse files Browse the repository at this point in the history
  • Loading branch information
tek committed Apr 7, 2024
1 parent 607dbdf commit b0a4bd9
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 88 deletions.
51 changes: 31 additions & 20 deletions grammar/exp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ module.exports = {
// expression
// ------------------------------------------------------------------------

_exp_op: $ => choice(
$._sym,
$._op_ticked,
alias($._prefix_dot, $.operator),
seq(
$._cond_qualified_op,
$._qsym,
),
),

exp_parens: $ => parens($, $._exp),

/**
Expand Down Expand Up @@ -44,15 +54,14 @@ module.exports = {
$,
$._infixexp,
choice(
$._ops,
$._exp_op,
$._operator_minus,
),
),

exp_section_right: $ => parens(
$,
choice(
prec('section-minus-shift', seq(alias('-', $.operator), $._infixexp)),
seq(alias($._operator_qual_dot_head, $.operator), $._infixexp),
seq($._ops, $._infixexp),
)
Expand Down Expand Up @@ -301,31 +310,33 @@ module.exports = {
field('arg', $._exp_apply_arg),
)),

exp_negation: $ => prec('negation', seq($._negation, prec('negation-reduce', $._infixexp))),

_infix_minus: $ => prec('operator-minus', alias('-', $.operator)),

_exp_op: $ => choice(
$._sym,
$._op_ticked,
alias($._prefix_dot, $.operator),
),
exp_negation: $ => prec('negation', seq('-', prec('negation-reduce', $._infixexp))),

/**
* This is a trick that allows negation to bind less tight than application and tighter than infix.
* The reduction to `exp_negation` conflicts with shifts for both other rules, and tree-sitter doesn't allow them to
* be ordered apply > negation > infix because all shifts must compare identically to the reduction in a
* multi-conflict.
*
* Splitting infix minus from the other variants ensures that apply and infix have separate conflicts with negation.
* The reason this works is that the second path is right-associative.
* It doesn't need a named shift prec to that, because it shares the `-` lookahead with `exp_negation`, so the shift
* is covered by the `exp_apply`/`exp_negation` conflict.
*
* Unfortunately, this has additional side effects:
* Left sections of minus with infix minus in their expression (`2 - 1 -`) are broken due to right-associativity, and
* negation in the first operand of `-` binds less tight (`- 2 - 1`).
*/
exp_infix: $ => choice(
prec.left('infix', seq(
prec('infix', seq(
field('left_operand', $._infixexp),
field('operator', $._exp_op),
field('right_operand', $._infixexp),
field('right_operand', prec('infix-reduce', $._infixexp)),
)),
seq(
field('left_operand', $._infixexp),
field('operator', $._infix_minus),
prec('infix-minus', field('right_operand', $._infixexp)),
),
seq(
field('left_operand', $._infixexp),
field('operator', $._qsym),
prec('infix-qualified', field('right_operand', $._infixexp)),
field('operator', $._operator_minus),
field('right_operand', prec('infix-reduce', $._infixexp)),
),
),

Expand Down
2 changes: 2 additions & 0 deletions grammar/externals.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ module.exports = {
$._cond_prefix_tilde,
$._cond_prefix_percent,

$._cond_qualified_op,

// Symbolic operators, producing text nodes.
// These are very difficult to parse in the grammar, because unlike most languages, Haskell's operators are not a
// finite set, and therefore cannot be lexically disambiguated from special operators like `->`.
Expand Down
23 changes: 11 additions & 12 deletions grammar/pat.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,25 +47,24 @@ module.exports = {
field('arg', $._pat_apply_arg),
)),

pat_negation: $ => prec('negation', seq($._negation, $._number)),
pat_negation: $ => prec('negation', seq('-', $._number)),

_pat_op: $ => choice(
$.constructor_operator,
$._conids_ticked,
),

pat_infix: $ => choice(
prec.left('infix', seq(
field('left_operand', $._infixpat),
field('operator', $._pat_op),
field('right_operand', $._infixpat),
pat_infix: $ => prec('infix', seq(
field('left_operand', $._infixpat),
field('operator', choice(
$._pat_op,
seq(
$._cond_qualified_op,
$._qconsym,
),
)),
seq(
field('left_operand', $._infixpat),
field('operator', $._qconsym),
prec('infix-qualified', field('right_operand', $._infixpat)),
),
),
field('right_operand', prec('infix-reduce', $._infixpat)),
)),

_infixpat: $ => choice(
$.pat_infix,
Expand Down
4 changes: 1 addition & 3 deletions grammar/precedences.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ module.exports = {
'prefix',
'apply',
'negation-reduce',
'infix-qualified',
'infix-minus',
'infix-reduce',
'infix',
'fun',
'implicit',
Expand Down Expand Up @@ -51,7 +50,6 @@ module.exports = {
[
'operator-minus',
'negation',
'section-minus-shift',
],

// ------------------------------------------------
Expand Down
Loading

0 comments on commit b0a4bd9

Please sign in to comment.