Skip to content

Commit

Permalink
Add implicit coefficient syntax
Browse files Browse the repository at this point in the history
Refs   #707.
  • Loading branch information
evhub committed Jan 7, 2023
1 parent 42fd75b commit 269de48
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 57 deletions.
37 changes: 27 additions & 10 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -467,10 +467,9 @@ In order of precedence, highest first, the operators supported in Coconut are:
====================== ==========================
Symbol(s) Associativity
====================== ==========================
f x n/a
await x n/a
.. n/a
** right
** right (allows unary)
f x n/a
+, -, ~ unary
*, /, //, %, @ left
+, - left
Expand All @@ -479,6 +478,7 @@ await x n/a
^ left
| left
:: n/a (lazy)
.. n/a
a `b` c, left (captures lambda)
all custom operators
?? left (short-circuits)
Expand Down Expand Up @@ -681,7 +681,7 @@ The `..>` and `<..` function composition pipe operators also have multi-arg, key

Note that `None`-aware function composition pipes don't allow either function to be `None`—rather, they allow the return of the first evaluated function to be `None`, in which case `None` is returned immediately rather than calling the next function.

The `..` operator has lower precedence than `await` but higher precedence than `**` while the `..>` pipe operators have a precedence directly higher than normal pipes.
The `..` operator has lower precedence than `::` but higher precedence than infix functions while the `..>` pipe operators have a precedence directly higher than normal pipes.

All function composition operators also have in-place versions (e.g. `..=`).

Expand Down Expand Up @@ -1835,16 +1835,25 @@ Lazy lists, where sequences are only evaluated when their contents are requested
**Python:**
_Can't be done without a complicated iterator comprehension in place of the lazy list. See the compiled code for the Python syntax._

### Implicit Function Application
### Implicit Function Application and Coefficients

Coconut supports implicit function application of the form `f x y`, which is compiled to `f(x, y)` (note: **not** `f(x)(y)` as is common in many languages with automatic currying). Implicit function application has a lower precedence than attribute access, slices, normal function calls, etc. but a higher precedence than `await`.
Coconut supports implicit function application of the form `f x y`, which is compiled to `f(x, y)` (note: **not** `f(x)(y)` as is common in many languages with automatic currying).

Supported arguments to implicit function application are highly restricted, and must be:
Additionally, if the first argument is not callable, then the result is multiplication rather than function application, such that `2 x` is equivalent to `2*x`.

Supported arguments are highly restricted, and must be:
- variables/attributes (e.g. `a.b`),
- literal constants (e.g. `True`), or
- number literals (e.g. `1.5`).
- literal constants (e.g. `True`),
- number literals (e.g. `1.5`), or
- one of the above followed by an exponent (e.g. `a**-5`).

For example, `f x 1` will work but `f x [1]`, `f x (1+2)`, and `f "abc"` will not. Strings are disallowed due to conflicting with [Python's implicit string concatenation](https://stackoverflow.com/questions/18842779/string-concatenation-without-operator).

Implicit function application and coefficient syntax is only intended for simple use cases—for more complex cases, use the standard multiplication operator `*`, standard function application, or [pipes](#pipes).

For example, `f x 1` will work but `f x [1]`, `f x (1+2)`, and `f "abc"` will not. Strings are disallowed due to conflicting with [Python's implicit string concatenation](https://stackoverflow.com/questions/18842779/string-concatenation-without-operator). Implicit function application is only intended for simple use cases—for more complex cases, use either standard function application or [pipes](#pipes).
Implicit function application and coefficient syntax has a lower precedence than `**` but a higher precedence than unary operators. As a result, `2 x**2 + 3 x` is equivalent to `2 * x**2 + 3 * x`.

Note that, due to potential confusion, `await` is not allowed in front of implicit function application and coefficient syntax. To use `await`, simply parenthesize the expression, as in `await (f x)`.

##### Examples

Expand All @@ -1859,6 +1868,10 @@ def p1(x) = x + 1
print <| p1 5
```

```coconut
quad = 5 x**2 + 3 x + 1
```

**Python:**
```coconut_python
def f(x, y): return (x, y)
Expand All @@ -1870,6 +1883,10 @@ def p1(x): return x + 1
print(p1(5))
```

```coconut_python
quad = 5 * x**2 + 3 * x + 1
```

### Anonymous Namedtuples

Coconut supports anonymous [`namedtuple`](https://docs.python.org/3/library/collections.html#collections.namedtuple) literals, such that `(a=1, b=2)` can be used just as `(1, 2)`, but with added names. Anonymous `namedtuple`s are always pickleable.
Expand Down
54 changes: 53 additions & 1 deletion __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ def _coconut_tco(func: _Tfunc) -> _Tfunc:
return func


# any changes here should also be made to safe_call below
# any changes here should also be made to safe_call and call_or_coefficient below
@_t.overload
def call(
_func: _t.Callable[[_T], _U],
Expand Down Expand Up @@ -364,6 +364,58 @@ def safe_call(
) -> Expected[_T]: ...


# based on call above
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[[_T], _U],
_x: _T,
) -> _U: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[[_T, _U], _V],
_x: _T,
_y: _U,
) -> _V: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[[_T, _U, _V], _W],
_x: _T,
_y: _U,
_z: _V,
) -> _W: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[_t.Concatenate[_T, _P], _U],
_x: _T,
*args: _t.Any,
) -> _U: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[_t.Concatenate[_T, _U, _P], _V],
_x: _T,
_y: _U,
*args: _t.Any,
) -> _V: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[_t.Concatenate[_T, _U, _V, _P], _W],
_x: _T,
_y: _U,
_z: _V,
*args: _t.Any,
) -> _W: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _t.Callable[..., _T],
*args: _t.Any,
) -> _T: ...
@_t.overload
def _coconut_call_or_coefficient(
_func: _T,
*args: _T,
) -> _T: ...


def recursive_iterator(func: _T_iter_func) -> _T_iter_func:
return func

Expand Down
2 changes: 1 addition & 1 deletion coconut/__coconut__.pyi
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from __coconut__ import *
from __coconut__ import _coconut_tail_call, _coconut_tco, _coconut_call_set_names, _coconut_handle_cls_kwargs, _coconut_handle_cls_stargs, _namedtuple_of, _coconut, _coconut_super, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose
from __coconut__ import _coconut_tail_call, _coconut_tco, _coconut_call_set_names, _coconut_handle_cls_kwargs, _coconut_handle_cls_stargs, _namedtuple_of, _coconut, _coconut_super, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose, _coconut_call_or_coefficient
15 changes: 13 additions & 2 deletions coconut/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,7 @@ def complain_on_err(self):
try:
yield
except ParseBaseException as err:
# don't reformat, since we might have gotten here because reformat failed
complain(self.make_parse_err(err, reformat=False, include_ln=False))
except CoconutException as err:
complain(err)
Expand Down Expand Up @@ -1397,6 +1398,11 @@ def reind_proc(self, inputstring, ignore_errors=False, **kwargs):

indent, line = split_leading_indent(line)
level += ind_change(indent)
if level < 0:
if not ignore_errors:
logger.log_lambda(lambda: "failed to reindent:\n" + inputstring)
complain("negative indentation level: " + repr(level))
level = 0

if line:
line = " " * self.tabideal * (level + int(is_fake)) + line
Expand All @@ -1407,13 +1413,18 @@ def reind_proc(self, inputstring, ignore_errors=False, **kwargs):
# handle indentation markers interleaved with comment/endline markers
comment, change_in_level = rem_and_count_indents(comment)
level += change_in_level
if level < 0:
if not ignore_errors:
logger.log_lambda(lambda: "failed to reindent:\n" + inputstring)
complain("negative indentation level: " + repr(level))
level = 0

line = (line + comment).rstrip()
out.append(line)

if not ignore_errors and level != 0:
logger.log_lambda(lambda: "failed to reindent:\n" + "\n".join(out))
complain(CoconutInternalException("non-zero final indentation level ", level))
logger.log_lambda(lambda: "failed to reindent:\n" + inputstring)
complain("non-zero final indentation level: " + repr(level))
return "\n".join(out)

def ln_comment(self, ln):
Expand Down
72 changes: 37 additions & 35 deletions coconut/compiler/grammar.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,21 +468,21 @@ def simple_kwd_assign_handle(tokens):
simple_kwd_assign_handle.ignore_one_token = True


def compose_item_handle(tokens):
def impl_call_item_handle(tokens):
"""Process implicit function application or coefficient syntax."""
internal_assert(len(tokens) >= 2, "invalid implicit call / coefficient tokens", tokens)
return "_coconut_call_or_coefficient(" + ", ".join(tokens) + ")"


def compose_expr_handle(tokens):
"""Process function composition."""
if len(tokens) == 1:
return tokens[0]
internal_assert(len(tokens) >= 1, "invalid function composition tokens", tokens)
return "_coconut_forward_compose(" + ", ".join(reversed(tokens)) + ")"


compose_item_handle.ignore_one_token = True


def impl_call_item_handle(tokens):
"""Process implicit function application."""
internal_assert(len(tokens) > 1, "invalid implicit function application tokens", tokens)
return tokens[0] + "(" + ", ".join(tokens[1:]) + ")"
compose_expr_handle.ignore_one_token = True


def tco_return_handle(tokens):
Expand Down Expand Up @@ -929,6 +929,7 @@ class Grammar(object):
namedexpr_test = Forward()
# for namedexpr locations only supported in Python 3.10
new_namedexpr_test = Forward()
lambdef = Forward()

negable_atom_item = condense(Optional(neg_minus) + atom_item)

Expand Down Expand Up @@ -1354,41 +1355,34 @@ class Grammar(object):
type_alias_stmt = Forward()
type_alias_stmt_ref = type_kwd.suppress() + setname + Optional(type_params) + equals.suppress() + typedef_test

impl_call_arg = disallow_keywords(reserved_vars) + (
await_expr = Forward()
await_expr_ref = await_kwd.suppress() + atom_item
await_item = await_expr | atom_item

factor = Forward()
unary = plus | neg_minus | tilde

power = condense(exp_dubstar + ZeroOrMore(unary) + await_item)

impl_call_arg = disallow_keywords(reserved_vars) + condense((
keyword_atom
| number
| dotted_refname
)
) + Optional(power))
impl_call = attach(
disallow_keywords(reserved_vars)
+ atom_item
+ OneOrMore(impl_call_arg),
impl_call_item_handle,
)
impl_call_item = (
atom_item + ~impl_call_arg
| impl_call
)

await_expr = Forward()
await_expr_ref = await_kwd.suppress() + impl_call_item
await_item = await_expr | impl_call_item

lambdef = Forward()

compose_item = attach(
tokenlist(
await_item,
dotdot + Optional(invalid_syntax(lambdef, "lambdas only allowed after composition pipe operators '..>' and '<..', not '..' (replace '..' with '<..' to fix)")),
allow_trailing=False,
), compose_item_handle,
factor <<= condense(
ZeroOrMore(unary) + (
impl_call
| await_item + Optional(power)
),
)

factor = Forward()
unary = plus | neg_minus | tilde
power = trace(condense(compose_item + Optional(exp_dubstar + factor)))
factor <<= condense(ZeroOrMore(unary) + power)

mulop = mul_star | div_slash | div_dubslash | percent | matrix_at
addop = plus | sub_minus
shift = lshift | rshift
Expand All @@ -1413,17 +1407,25 @@ class Grammar(object):

chain_expr = attach(tokenlist(or_expr, dubcolon, allow_trailing=False), chain_handle)

compose_expr = attach(
tokenlist(
chain_expr,
dotdot + Optional(invalid_syntax(lambdef, "lambdas only allowed after composition pipe operators '..>' and '<..', not '..' (replace '..' with '<..' to fix)")),
allow_trailing=False,
), compose_expr_handle,
)

infix_op <<= backtick.suppress() + test_no_infix + backtick.suppress()
infix_expr = Forward()
infix_item = attach(
Group(Optional(chain_expr))
Group(Optional(compose_expr))
+ OneOrMore(
infix_op + Group(Optional(lambdef | chain_expr)),
infix_op + Group(Optional(lambdef | compose_expr)),
),
infix_handle,
)
infix_expr <<= (
chain_expr + ~backtick
compose_expr + ~backtick
| infix_item
)

Expand Down Expand Up @@ -2435,7 +2437,7 @@ def get_tre_return_grammar(self, func_name):

unsafe_equals = Literal("=")

kwd_err_msg = attach(any_keyword_in(keyword_vars), kwd_err_msg_handle)
kwd_err_msg = attach(any_keyword_in(keyword_vars + reserved_vars), kwd_err_msg_handle)
parse_err_msg = (
start_marker + (
fixto(end_of_line, "misplaced newline (maybe missing ':')")
Expand Down
2 changes: 1 addition & 1 deletion coconut/compiler/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ async def __anext__(self):
# (extra_format_dict is to keep indentation levels matching)
extra_format_dict = dict(
# when anything is added to this list it must also be added to *both* __coconut__ stub files
underscore_imports="{tco_comma}{call_set_names_comma}{handle_cls_args_comma}_namedtuple_of, _coconut, _coconut_super, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose".format(**format_dict),
underscore_imports="{tco_comma}{call_set_names_comma}{handle_cls_args_comma}_namedtuple_of, _coconut, _coconut_super, _coconut_Expected, _coconut_MatchError, _coconut_iter_getitem, _coconut_base_compose, _coconut_forward_compose, _coconut_back_compose, _coconut_forward_star_compose, _coconut_back_star_compose, _coconut_forward_dubstar_compose, _coconut_back_dubstar_compose, _coconut_pipe, _coconut_star_pipe, _coconut_dubstar_pipe, _coconut_back_pipe, _coconut_back_star_pipe, _coconut_back_dubstar_pipe, _coconut_none_pipe, _coconut_none_star_pipe, _coconut_none_dubstar_pipe, _coconut_bool_and, _coconut_bool_or, _coconut_none_coalesce, _coconut_minus, _coconut_map, _coconut_partial, _coconut_get_function_match_error, _coconut_base_pattern_func, _coconut_addpattern, _coconut_sentinel, _coconut_assert, _coconut_raise, _coconut_mark_as_match, _coconut_reiterable, _coconut_self_match_types, _coconut_dict_merge, _coconut_exec, _coconut_comma_op, _coconut_multi_dim_arr, _coconut_mk_anon_namedtuple, _coconut_matmul, _coconut_py_str, _coconut_flatten, _coconut_multiset, _coconut_back_none_pipe, _coconut_back_none_star_pipe, _coconut_back_none_dubstar_pipe, _coconut_forward_none_compose, _coconut_back_none_compose, _coconut_forward_none_star_compose, _coconut_back_none_star_compose, _coconut_forward_none_dubstar_compose, _coconut_back_none_dubstar_compose, _coconut_call_or_coefficient".format(**format_dict),
import_typing=pycondition(
(3, 5),
if_ge="import typing",
Expand Down
6 changes: 6 additions & 0 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -1822,5 +1822,11 @@ def _coconut_multi_dim_arr(arrs, dim):
arr_dims.append(dim)
max_arr_dim = _coconut.max(arr_dims)
return _coconut_concatenate(arrs, max_arr_dim - dim)
def _coconut_call_or_coefficient(func, *args):
if _coconut.callable(func):
return func(*args)
for x in args:
func *= x
return func
_coconut_self_match_types = {self_match_types}
_coconut_Expected, _coconut_MatchError, _coconut_count, _coconut_enumerate, _coconut_flatten, _coconut_filter, _coconut_ident, _coconut_map, _coconut_multiset, _coconut_range, _coconut_reiterable, _coconut_reversed, _coconut_starmap, _coconut_tee, _coconut_zip, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, count, enumerate, flatten, filter, ident, map, multiset, range, reiterable, reversed, starmap, tee, zip, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile

0 comments on commit 269de48

Please sign in to comment.