Skip to content

Commit

Permalink
Alter precedence of anonymous functions to be more sane
Browse files Browse the repository at this point in the history
It's just weird that `func(x) x+y for y in ..10` was a vector of 10 
anonymous functions. Nobody would expect that when writing such a piece 
of code.
  • Loading branch information
jonathanhogg committed Dec 28, 2024
1 parent 4d0d729 commit e7e0393
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 15 deletions.
33 changes: 23 additions & 10 deletions docs/language.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,24 +872,37 @@ with the syntax:
func (parameter《=default》《, parameter…》) body
```

In the grammar rules, anonymous functions sit above inline `if`/`else`
expressions, but below inline `for` and `where` expressions. Therefore, the
following creates a vector of 10 anonymous functions, each of which multiplies
by a different value, not a single function that does 10 multiplications in
a loop:
The body of an anonymous function may only contain *in-line* expressions. In
the grammar, anonymous functions have higher precedence than `;` composition
but lower than inline `for` and `where` expressions. Therefore, the following
defines a function containing a for loop, as might be expected:

```flitter
let f = func(x) x*y for y in ..10
```

Note that as function calls are allowed to accept a vector of functions, it is
valid to call `f`. So `f(3)` will still evaluate to `0;3;6;9;12;15;18;21;24;27`.
However, the following binds `f` to a vector consisting of an anonymous
function that returns its argument as-is, and the numbers *1* and *2*:

It is wise to use parentheses to make the precedence explicit.
```flitter
let f = func(x) x;1;2
```

Note that calling `f` is *not* itself an error, as a call to a vector is valid.
The anonymous function will be evaluated and the attempted calls to the two
numbers will log evaluation errors and return `null`. So `f(0)` will evaluate
to `0`.

An anonymous function being returned by another anonymous function must be
parenthesised, e.g.:

```flitter
let f = func(x) (func(y) x + y)
```

As with regular functions, any captured names are bound at the point of
definition. An anonymous function cannot be recursive as it has no function
name to use within the body.
definition. An anonymous function cannot call itself recursively as there is
no bound function name to use within the body.

## Template function calls

Expand Down
8 changes: 5 additions & 3 deletions src/flitter/language/grammar.lark
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,17 @@ bindings : binding+ -> tuple

binding : name_list "=" composition -> poly_binding

?composition : comprehension
?composition : anonymous
| compositions -> sequence

compositions : comprehension (";" comprehension)+ -> tuple
compositions : anonymous (";" anonymous)+ -> tuple

?anonymous : comprehension
| "func" _LPAREN parameters _RPAREN comprehension -> anonymous_function

?comprehension : conditional
| comprehension "for" name_list "in" conditional -> inline_loop
| comprehension "where" inline_bindings -> inline_let
| "func" _LPAREN parameters _RPAREN conditional -> anonymous_function

inline_bindings: inline_binding+ -> tuple

Expand Down
2 changes: 2 additions & 0 deletions src/flitter/language/tree.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,8 @@ cdef class Call(Expression):
results.append(func(*vector_args, **kwargs))
except Exception as exc:
context.errors.add(f"Error calling {func.__name__}: {str(exc)}")
else:
context.errors.add(f"{func!r} is not callable")
return Literal(Vector._compose(results))
if isinstance(function, Literal) and len(args) == 1:
if (<Literal>function).value == static_builtins['ceil']:
Expand Down
4 changes: 4 additions & 0 deletions src/flitter/language/vm.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,10 @@ cdef inline void call_helper(Context context, VectorStack stack, object function
Py_INCREF(<object>obj)
PyTuple_SET_ITEM(context_args, i+1, <object>obj)
args = context_args
elif not callable(function):
PySet_Add(context.errors, f"{function!r} is not callable")
push(stack, null_)
return
if record_stats:
call_duration = -perf_counter()
try:
Expand Down
41 changes: 39 additions & 2 deletions tests/test_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class TestLanguageFeatures(unittest.TestCase):
It is assumed that all of the examples can be fully reduced to a literal by the simplifier.
"""

def assertCodeOutput(self, code, output, with_errors=None, **names):
def assertCodeOutput(self, code, output, with_errors=None, skip_simplifier=False, **names):
top = parse(code.strip())
output = output.strip()
if with_errors is None:
Expand All @@ -103,6 +103,8 @@ def assertCodeOutput(self, code, output, with_errors=None, **names):
vm_output = '\n'.join(repr(node) for node in top.compile(initial_lnames=tuple(names)).run(vm_context).root.children)
self.assertEqual(vm_output, output, msg="VM output is incorrect")
self.assertEqual(vm_context.errors, with_errors)
if skip_simplifier:
return
simplified_top, simplifier_context = top.simplify(static=names, return_context=True)
self.assertEqual(simplifier_context.errors, with_errors)
expr = simplified_top.body
Expand Down Expand Up @@ -328,7 +330,7 @@ def test_nested_functions(self):
!fib x=55
""")

def test_anonymous_functions(self):
def test_anonymous_function(self):
"""Note that this is statically reducible because `map` is inlined and so the
anonymous function is bound to `f`, which therefore becomes a function name"""
self.assertCodeOutput(
Expand All @@ -343,6 +345,41 @@ def test_anonymous_functions(self):
!doubled x=0;2;4;6;8;10;12;14;16;18
""")

def test_anonymous_function_with_where(self):
self.assertCodeOutput(
"""
let f = func(x) x+y where y=x*x
!foo bar=f(10)
""",
"""
!foo bar=110
"""
)

def test_anonymous_function_returning_anonymous_function(self):
self.assertCodeOutput(
"""
let f = func(x) (func(y) x + y)
!foo bar=f(10)(5)
""",
"""
!foo bar=15
"""
)

def test_accidental_anonymous_vector(self):
self.assertCodeOutput(
"""
let f = func(x) x;1;2
!foo bar=f(0)
""",
"""
!foo bar=0
""",
with_errors={'1.0 is not callable', '2.0 is not callable'},
skip_simplifier=True
)

def test_some_deliberately_obtuse_behaviour(self):
self.assertCodeOutput(
"""
Expand Down

0 comments on commit e7e0393

Please sign in to comment.