Skip to content

Commit b995c08

Browse files
committed
Make infix expressions left-associative (again)
After trying out both I think I prefer this and imagine it's what most people will expect. I've left my flip-flopping here in version-control history for posterity.
1 parent 6e45f51 commit b995c08

File tree

3 files changed

+55
-39
lines changed

3 files changed

+55
-39
lines changed

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Here's another example:
156156
```
157157
{
158158
cons: b => a => { :a, :b }
159-
list: 1 cons 2 cons 3 // evaluates to `{ 1, { 2, 3 } }`
159+
list: 1 cons (2 cons 3) // evaluates to `{ 1, { 2, 3 } }`
160160
}
161161
```
162162

@@ -174,10 +174,8 @@ in the standard library are the functions`|>` (pipe) and `>>` (flow):
174174
}
175175
```
176176

177-
All binary operations are currently right-associative and there is no operator
178-
precedence. This means some arithmetic expressions may not behave the way you're
179-
used to; for example `1 - 2 - 3` means `1 - (2 - 3)` (2), not `(1 - 2) - 3`
180-
(-4). Use of parentheses is encouraged.
177+
All binary operations are currently left-associative and there is no operator
178+
precedence. Use of parentheses is encouraged.
181179

182180
#### Keywords
183181

src/end-to-end.test.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
168168
[`:integer.subtract(-1)(-1)`, either.makeRight('0')],
169169
[`-1 - -1`, either.makeRight('0')],
170170
[`2 - 1`, either.makeRight('1')],
171-
[
172-
`1 - 2 - 3`,
173-
either.makeRight(
174-
'2', // with traditional operator associativity this would be `-4`
175-
),
176-
],
171+
[`1 - 2 - 3`, either.makeRight('-4')],
177172
[`1 - (2 - 3)`, either.makeRight('2')],
178173
[`(1 - 2) - 3`, either.makeRight('-4')],
179174
[':flow(:atom.append(b))(:atom.append(a))(z)', either.makeRight('zab')],
@@ -322,7 +317,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
322317
[`(a => (1 + :a))(1)`, either.makeRight('2')],
323318
[`2 |> (a => :a)`, either.makeRight('2')],
324319
[`a atom.append b atom.append c`, either.makeRight('abc')],
325-
[`a atom.append c atom.prepend b`, either.makeRight('abc')],
320+
[`b atom.append c atom.prepend a`, either.makeRight('abc')],
326321
[`(b atom.append c) atom.prepend a`, either.makeRight('abc')],
327322
[`a atom.append (c atom.prepend b)`, either.makeRight('abc')],
328323
[
@@ -345,7 +340,7 @@ testCases(endToEnd, code => code)('end-to-end tests', [
345340
(
346341
PATH
347342
|> :context.environment.lookup
348-
>> :match({
343+
|> :match({
349344
none: _ => "$PATH not set"
350345
some: :atom.prepend("PATH=")
351346
})
@@ -385,8 +380,8 @@ testCases(endToEnd, code => code)('end-to-end tests', [
385380
)(0)`,
386381
either.makeRight('10'),
387382
],
388-
[`a |> :atom.append(b) >> :atom.append(c)`, either.makeRight('abc')],
389-
[`(a |> :atom.append(b)) |> :atom.append(c)`, either.makeRight('abc')],
383+
[`a |> :atom.append(b) |> :atom.append(c)`, either.makeRight('abc')],
384+
[`a |> (:atom.append(b) >> :atom.append(c))`, either.makeRight('abc')],
390385
[`:|>(:>>(:atom.append(c))(:atom.append(b)))(a)`, either.makeRight('abc')],
391386
[
392387
`{
@@ -403,4 +398,29 @@ testCases(endToEnd, code => code)('end-to-end tests', [
403398
}.abc`,
404399
either.makeRight('abc'),
405400
],
401+
[
402+
`{
403+
nested_option: {
404+
tag: some,
405+
value: {
406+
tag: some,
407+
value: {
408+
tag: some,
409+
value: "it works!"
410+
}
411+
}
412+
}
413+
output: :nested_option match {
414+
none: unreachable
415+
some: :identity
416+
} match {
417+
none: unreachable
418+
some: :identity
419+
} match {
420+
none: unreachable
421+
some: :identity
422+
}
423+
}.output`,
424+
either.makeRight('it works!'),
425+
],
406426
])

src/language/parsing/expression.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -87,58 +87,56 @@ const isOperand = (value: InfixToken | undefined): value is InfixOperand =>
8787
const isOperator = (value: InfixToken | undefined): value is InfixOperator =>
8888
Array.isArray(value)
8989

90-
const appendToArray = <A>(b: A, init: readonly A[]): readonly [A, ...A[]] =>
91-
// Unfortunately TypeScript can't reason through this on its own:
92-
[...init, b] as readonly A[] as readonly [A, ...A[]]
93-
9490
const infixTokensToExpression = (
9591
operation: InfixOperation,
9692
): Molecule | Atom => {
9793
const firstToken = operation[0]
9894
if (operation.length === 1 && isOperand(firstToken)) {
9995
return firstToken
10096
} else {
101-
const rightmostOperationRHS = operation[operation.length - 1]
102-
if (rightmostOperationRHS === undefined) {
97+
const leftmostOperationLHS = operation[0]
98+
if (leftmostOperationLHS === undefined) {
10399
throw new Error('Infix operation was empty. This is a bug!')
104100
}
105-
if (!isOperand(rightmostOperationRHS)) {
101+
if (!isOperand(leftmostOperationLHS)) {
106102
throw new Error(
107-
'Rightmost token in infix operation was not an operand. This is a bug!',
103+
'Leftmost token in infix operation was not an operand. This is a bug!',
108104
)
109105
}
110-
const rightmostOperator = operation[operation.length - 2]
111-
if (!isOperator(rightmostOperator)) {
106+
107+
const leftmostOperator = operation[1]
108+
if (!isOperator(leftmostOperator)) {
112109
throw new Error(
113-
'Could not find rightmost operator in infix operation. This is a bug!',
110+
'Could not find leftmost operator in infix operation. This is a bug!',
114111
)
115112
}
116113

117-
const rightmostOperationLHS = operation[operation.length - 3]
118-
if (!isOperand(rightmostOperationLHS)) {
114+
const leftmostOperationRHS = operation[2]
115+
if (!isOperand(leftmostOperationRHS)) {
119116
throw new Error(
120-
'Missing left-hand side of infix operation. This is a bug!',
117+
'Missing right-hand side of infix operation. This is a bug!',
121118
)
122119
}
123120

124-
const rightmostFunction = trailingIndexesAndArgumentsToExpression(
125-
{ 0: '@lookup', key: rightmostOperator[0] },
126-
rightmostOperator[1],
121+
const leftmostFunction = trailingIndexesAndArgumentsToExpression(
122+
{ 0: '@lookup', key: leftmostOperator[0] },
123+
leftmostOperator[1],
127124
)
128125

129-
const reducedRightmostOperation: Molecule = {
126+
const reducedLeftmostOperation: Molecule = {
130127
0: '@apply',
131128
function: {
132129
0: '@apply',
133-
function: rightmostFunction,
134-
argument: rightmostOperationRHS,
130+
function: leftmostFunction,
131+
argument: leftmostOperationRHS,
135132
},
136-
argument: rightmostOperationLHS,
133+
argument: leftmostOperationLHS,
137134
}
138135

139-
return infixTokensToExpression(
140-
appendToArray(reducedRightmostOperation, operation.slice(0, -3)),
141-
)
136+
return infixTokensToExpression([
137+
reducedLeftmostOperation,
138+
...operation.slice(3),
139+
])
142140
}
143141
}
144142

0 commit comments

Comments
 (0)