Skip to content

Commit bc88383

Browse files
authored
Support letfn Special Form (#473)
* Support `letfn` Special Form * Stop the spinning * Fix everything * letfn* correctly * Lint it out * Docstrings * Letfn sequential destructuring tests * letfn associative destructuring tests
1 parent e985a8a commit bc88383

File tree

10 files changed

+576
-2
lines changed

10 files changed

+576
-2
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
shell: /bin/bash -leo pipefail
1919
environment:
2020
TOX_NUM_CORES: 2
21+
TOX_PARALLEL_NO_SPINNER: 1
2122
TOX_SHOW_OUTPUT: "True"
2223
TOX_SKIP_ENV: pypy3
2324
command: |

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Added support for Shebang-style line comments (#469)
1010
* Added multiline REPL support using `prompt-toolkit` (#467)
1111
* Added node syntactic location (statement or expression) to Basilisp AST nodes emitted by the analyzer (#463)
12+
* Added `letfn` special form (#473)
1213

1314
### Changed
1415
* Change the default user namespace to `basilisp.user` (#466)

src/basilisp/core.lpy

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3980,6 +3980,22 @@
39803980
(mapcat destructure))]
39813981
~@body))
39823982

3983+
(defmacro letfn
3984+
"Let form specifically for function definitions.
3985+
3986+
Functions are defined as bindings:
3987+
3988+
[(plus-two [x] (+ (plus-one x) 1))
3989+
(plus-one [x] (+ x 1))]
3990+
3991+
Functions defined in `letfn` bindings may refer to each other regardless
3992+
of their order of definition."
3993+
[bindings & body]
3994+
`(letfn* [~@(mapcat (fn [fn-body]
3995+
[(first fn-body) (cons `fn fn-body)])
3996+
bindings)]
3997+
~@body))
3998+
39833999
(defn ^:private loop-with-destructuring
39844000
"Take a loop definition (an binding vector and 0 or more body
39854001
expressions) whose binding vector may or may not require destructuring

src/basilisp/lang/compiler/analyzer.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
SYM_DYNAMIC_META_KEY,
5858
SYM_MACRO_META_KEY,
5959
SYM_MUTABLE_META_KEY,
60+
SYM_NO_WARN_ON_SHADOW_META_KEY,
6061
SYM_NO_WARN_WHEN_UNUSED_META_KEY,
6162
SYM_PROPERTY_META_KEY,
6263
SYM_STATICMETHOD_META_KEY,
@@ -438,6 +439,13 @@ def put_new_symbol( # pylint: disable=too-many-arguments
438439
previously checked case, so this is a simple way of disabling these
439440
warnings for those cases.
440441
442+
There are cases where undesired warnings may be triggered non-locally,
443+
so the Python keyword arguments cannot be used to suppress unwanted
444+
warnings. For these cases, symbols may include the `:no-warn-on-shadow`
445+
metadata key to indicate that warnings for shadowing names from outer
446+
scopes should be suppressed. It is not currently possible to suppress
447+
Var shadowing warnings at the symbol level.
448+
441449
If WARN_ON_SHADOWED_NAME compiler option is active and the
442450
warn_on_shadowed_name keyword argument is True, then a warning will be
443451
emitted if a local name is shadowed by another local name. Note that
@@ -447,7 +455,16 @@ def put_new_symbol( # pylint: disable=too-many-arguments
447455
warn_on_shadowed_var keyword argument is True, then a warning will be
448456
emitted if a named var is shadowed by a local name."""
449457
st = self.symbol_table
450-
if warn_on_shadowed_name and self.warn_on_shadowed_name:
458+
no_warn_on_shadow = (
459+
Maybe(s.meta)
460+
.map(lambda m: m.val_at(SYM_NO_WARN_ON_SHADOW_META_KEY, False))
461+
.or_else_get(False)
462+
)
463+
if (
464+
not no_warn_on_shadow
465+
and warn_on_shadowed_name
466+
and self.warn_on_shadowed_name
467+
):
451468
if st.find_symbol(s) is not None:
452469
logger.warning(f"name '{s}' shadows name from outer scope")
453470
if (
@@ -1840,6 +1857,126 @@ def _let_ast(ctx: AnalyzerContext, form: ISeq) -> Let:
18401857
)
18411858

18421859

1860+
def __letfn_fn_body(ctx: AnalyzerContext, form: ISeq) -> Fn:
1861+
"""Produce an `Fn` node for a `letfn*` special form.
1862+
1863+
`letfn*` forms use `let*`-like bindings. Each function binding name is
1864+
added to the symbol table as a forward declaration before analyzing the
1865+
function body. The function bodies are defined as
1866+
1867+
(fn* name
1868+
[...]
1869+
...)
1870+
1871+
When the `name` is added to the symbol table for the function, a warning
1872+
will be produced because it will previously have been defined in the
1873+
`letfn*` binding scope. This function adds `:no-warn-on-shadow` metadata to
1874+
the function name symbol to disable the compiler warning."""
1875+
fn_sym = form.first
1876+
1877+
fn_name = runtime.nth(form, 1)
1878+
if not isinstance(fn_name, sym.Symbol):
1879+
raise AnalyzerException("letfn function name must be a symbol", form=fn_name)
1880+
1881+
fn_rest = runtime.nthrest(form, 2)
1882+
1883+
fn_body = _analyze_form(
1884+
ctx,
1885+
fn_rest.cons(
1886+
fn_name.with_meta(
1887+
(fn_name.meta or lmap.Map.empty()).assoc(
1888+
SYM_NO_WARN_ON_SHADOW_META_KEY, True
1889+
)
1890+
)
1891+
).cons(fn_sym),
1892+
)
1893+
1894+
if not isinstance(fn_body, Fn):
1895+
raise AnalyzerException(
1896+
"letfn bindings must be functions", form=form, lisp_ast=fn_body
1897+
)
1898+
1899+
return fn_body
1900+
1901+
1902+
def _letfn_ast( # pylint: disable=too-many-locals
1903+
ctx: AnalyzerContext, form: ISeq
1904+
) -> LetFn:
1905+
assert form.first == SpecialForm.LETFN
1906+
nelems = count(form)
1907+
1908+
if nelems < 2:
1909+
raise AnalyzerException(
1910+
"letfn forms must have bindings vector and 0 or more body forms", form=form
1911+
)
1912+
1913+
bindings = runtime.nth(form, 1)
1914+
if not isinstance(bindings, vec.Vector):
1915+
raise AnalyzerException("letfn bindings must be a vector", form=bindings)
1916+
elif len(bindings) % 2 != 0:
1917+
raise AnalyzerException(
1918+
"letfn bindings must appear in name-value pairs", form=bindings
1919+
)
1920+
1921+
with ctx.new_symbol_table("letfn"):
1922+
# Generate empty Binding nodes to put into the symbol table
1923+
# as forward declarations. All functions in letfn* forms may
1924+
# refer to all other functions regardless of order of definition.
1925+
empty_binding_nodes = []
1926+
for name, value in partition(bindings, 2):
1927+
if not isinstance(name, sym.Symbol):
1928+
raise AnalyzerException(
1929+
"letfn binding name must be a symbol", form=name
1930+
)
1931+
1932+
if not isinstance(value, llist.List):
1933+
raise AnalyzerException(
1934+
"letfn binding value must be a list", form=value
1935+
)
1936+
1937+
binding = Binding(
1938+
form=name,
1939+
name=name.name,
1940+
local=LocalType.LETFN,
1941+
init=_const_node(ctx, None),
1942+
children=vec.v(INIT),
1943+
env=ctx.get_node_env(),
1944+
)
1945+
empty_binding_nodes.append((name, value, binding))
1946+
ctx.put_new_symbol(
1947+
name, binding,
1948+
)
1949+
1950+
# Once we've generated all of the filler Binding nodes, analyze the
1951+
# function bodies and replace the Binding nodes with full nodes.
1952+
binding_nodes = []
1953+
for fn_name, fn_def, binding in empty_binding_nodes:
1954+
fn_body = __letfn_fn_body(ctx, fn_def)
1955+
new_binding = binding.assoc(init=fn_body)
1956+
binding_nodes.append(new_binding)
1957+
ctx.put_new_symbol(
1958+
fn_name,
1959+
new_binding,
1960+
warn_on_shadowed_name=False,
1961+
warn_on_shadowed_var=False,
1962+
)
1963+
1964+
letfn_body = runtime.nthrest(form, 2)
1965+
stmts, ret = _body_ast(ctx, letfn_body)
1966+
return LetFn(
1967+
form=form,
1968+
bindings=vec.vector(binding_nodes),
1969+
body=Do(
1970+
form=letfn_body,
1971+
statements=vec.vector(stmts),
1972+
ret=ret,
1973+
is_body=True,
1974+
env=ctx.get_node_env(),
1975+
),
1976+
env=ctx.get_node_env(pos=ctx.syntax_position),
1977+
)
1978+
1979+
18431980
def _loop_ast(ctx: AnalyzerContext, form: ISeq) -> Loop:
18441981
assert form.first == SpecialForm.LOOP
18451982
nelems = count(form)
@@ -2185,6 +2322,7 @@ def _var_ast(ctx: AnalyzerContext, form: ISeq) -> VarRef:
21852322
SpecialForm.IMPORT: _import_ast,
21862323
SpecialForm.INTEROP_CALL: _host_interop_ast,
21872324
SpecialForm.LET: _let_ast,
2325+
SpecialForm.LETFN: _letfn_ast,
21882326
SpecialForm.LOOP: _loop_ast,
21892327
SpecialForm.QUOTE: _quote_ast,
21902328
SpecialForm.RECUR: _recur_ast,

src/basilisp/lang/compiler/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class SpecialForm:
1515
INTEROP_CALL = sym.symbol(".")
1616
INTEROP_PROP = sym.symbol(".-")
1717
LET = sym.symbol("let*")
18+
LETFN = sym.symbol("letfn*")
1819
LOOP = sym.symbol("loop*")
1920
QUOTE = sym.symbol("quote")
2021
RECUR = sym.symbol("recur")
@@ -141,6 +142,7 @@ class SpecialForm:
141142
SYM_MACRO_META_KEY = kw.keyword("macro")
142143
SYM_MUTABLE_META_KEY = kw.keyword("mutable")
143144
SYM_NO_WARN_ON_REDEF_META_KEY = kw.keyword("no-warn-on-redef")
145+
SYM_NO_WARN_ON_SHADOW_META_KEY = kw.keyword("no-warn-on-shadow")
144146
SYM_NO_WARN_WHEN_UNUSED_META_KEY = kw.keyword("no-warn-when-unused")
145147
SYM_REDEF_META_KEY = kw.keyword("redef")
146148
SYM_STATICMETHOD_META_KEY = kw.keyword("staticmethod")

src/basilisp/lang/compiler/generator.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
Import,
6565
Invoke,
6666
Let,
67+
LetFn,
6768
Local,
6869
LocalType,
6970
Loop,
@@ -113,6 +114,7 @@
113114
_IF_RESULT_PREFIX = "if_result"
114115
_IF_TEST_PREFIX = "if_test"
115116
_LET_RESULT_PREFIX = "let_result"
117+
_LETFN_RESULT_PREFIX = "letfn_result"
116118
_LOOP_RESULT_PREFIX = "loop_result"
117119
_MULTI_ARITY_ARG_NAME = "multi_arity_args"
118120
_SET_BANG_TEMP_PREFIX = "set_bang_val"
@@ -1607,6 +1609,53 @@ def _let_to_py_ast(ctx: GeneratorContext, node: Let) -> GeneratedPyAST:
16071609
return GeneratedPyAST(node=_noop_node(), dependencies=let_body_ast)
16081610

16091611

1612+
@_with_ast_loc_deps
1613+
def _letfn_to_py_ast(ctx: GeneratorContext, node: LetFn) -> GeneratedPyAST:
1614+
"""Return a Python AST Node for a `letfn*` expression."""
1615+
assert node.op == NodeOp.LETFN
1616+
1617+
with ctx.new_symbol_table("letfn"):
1618+
binding_names = []
1619+
for binding in node.bindings:
1620+
binding_name = genname(munge(binding.name))
1621+
ctx.symbol_table.new_symbol(
1622+
sym.symbol(binding.name), binding_name, LocalType.LET
1623+
)
1624+
binding_names.append((binding_name, binding))
1625+
1626+
letfn_body_ast: List[ast.AST] = []
1627+
for binding_name, binding in binding_names:
1628+
init_node = binding.init
1629+
assert init_node is not None
1630+
init_ast = gen_py_ast(ctx, init_node)
1631+
letfn_body_ast.extend(init_ast.dependencies)
1632+
letfn_body_ast.append(
1633+
ast.Assign(
1634+
targets=[ast.Name(id=binding_name, ctx=ast.Store())],
1635+
value=init_ast.node,
1636+
)
1637+
)
1638+
1639+
letfn_result_name = genname(_LETFN_RESULT_PREFIX)
1640+
body_ast = _synthetic_do_to_py_ast(ctx, node.body)
1641+
letfn_body_ast.extend(map(statementize, body_ast.dependencies))
1642+
1643+
if node.env.pos == NodeSyntacticPosition.EXPR:
1644+
letfn_body_ast.append(
1645+
ast.Assign(
1646+
targets=[ast.Name(id=letfn_result_name, ctx=ast.Store())],
1647+
value=body_ast.node,
1648+
)
1649+
)
1650+
return GeneratedPyAST(
1651+
node=ast.Name(id=letfn_result_name, ctx=ast.Load()),
1652+
dependencies=letfn_body_ast,
1653+
)
1654+
else:
1655+
letfn_body_ast.append(body_ast.node)
1656+
return GeneratedPyAST(node=_noop_node(), dependencies=letfn_body_ast)
1657+
1658+
16101659
@_with_ast_loc_deps
16111660
def _loop_to_py_ast(ctx: GeneratorContext, node: Loop) -> GeneratedPyAST:
16121661
"""Return a Python AST Node for a `loop*` expression."""
@@ -2659,7 +2708,7 @@ def _const_node_to_py_ast(ctx: GeneratorContext, lisp_ast: Const) -> GeneratedPy
26592708
NodeOp.IMPORT: _import_to_py_ast,
26602709
NodeOp.INVOKE: _invoke_to_py_ast,
26612710
NodeOp.LET: _let_to_py_ast,
2662-
NodeOp.LETFN: None, # type: ignore
2711+
NodeOp.LETFN: _letfn_to_py_ast,
26632712
NodeOp.LOCAL: _local_sym_to_py_ast,
26642713
NodeOp.LOOP: _loop_to_py_ast,
26652714
NodeOp.MAP: _map_to_py_ast,

src/basilisp/lang/compiler/nodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,7 @@ class WithMeta(Node[LispForm]):
855855
Import,
856856
Invoke,
857857
Let,
858+
LetFn,
858859
Loop,
859860
Quote,
860861
Recur,

src/basilisp/lang/runtime.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
_INTEROP_CALL = sym.symbol(".")
8787
_INTEROP_PROP = sym.symbol(".-")
8888
_LET = sym.symbol("let*")
89+
_LETFN = sym.symbol("letfn*")
8990
_LOOP = sym.symbol("loop*")
9091
_QUOTE = sym.symbol("quote")
9192
_RECUR = sym.symbol("recur")
@@ -106,6 +107,7 @@
106107
_INTEROP_CALL,
107108
_INTEROP_PROP,
108109
_LET,
110+
_LETFN,
109111
_LOOP,
110112
_QUOTE,
111113
_RECUR,

0 commit comments

Comments
 (0)