Skip to content

Commit bab2043

Browse files
authored
Disallow set!s on non-threadbound Vars (#638)
* Disallow set!s on non-threadbound Vars * Update changelog * Simplifications * Tests and such * Fix that one test * Improve code generation logic * Simplify
1 parent 451eb67 commit bab2043

File tree

14 files changed

+397
-140
lines changed

14 files changed

+397
-140
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
* Added a bootstrapping function for easily bootstrapping Basilisp projects from Python (#620)
1010
* Added support for watchers and validator functions on Atoms and Vars (#627)
1111
* Added support for Taps (#631)
12-
* Added support for hierarchies (#???)
12+
* Added support for hierarchies (#633)
1313

1414
### Changed
1515
* PyTest is now an optional extra dependency, rather than a required dependency (#622)
@@ -18,9 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
### Fixed
1919
* Fixed a bug where `seq`ing co-recursive lazy sequences would cause a stack overflow (#632)
2020
* Fixed a spurious failure in the test runner and switched to using macro forms for test line numbers (#631)
21+
* Fixed a bug that allowed dynamic Vars to be `set!` even if they weren't thread-bound (#638)
2122

2223
### Removed
23-
* Removed Click as a dependency in favor of builtin `argparse` (#622, #624)
24+
* Removed Click as a dependency in favor of builtin `argparse` (#622, #624, #636)
2425
* Removed Atomos as a dependency in favor of `readerwriterlock` (#624)
2526

2627
## [v0.1.dev15]

mypy.ini

Lines changed: 0 additions & 18 deletions
This file was deleted.

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,5 +131,18 @@ skip = [
131131
"pip-wheel-metadata",
132132
]
133133

134+
[tool.mypy]
135+
check_untyped_defs = true
136+
warn_unused_ignores = true
137+
138+
[[tool.mypy.overrides]]
139+
module = [
140+
"astor.*",
141+
"prompt_toolkit.*",
142+
"pygments.*",
143+
"pytest.*"
144+
]
145+
ignore_missing_imports = true
146+
134147
[tool.pytest.ini_options]
135148
junit_family = "legacy"

src/basilisp/cli.py

Lines changed: 185 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
import traceback
77
import types
8-
from typing import Any, Callable, Optional, Sequence
8+
from typing import Any, Callable, Optional, Sequence, Type
99

1010
from basilisp import main as basilisp
1111
from basilisp.lang import compiler as compiler
@@ -20,6 +20,11 @@
2020
STDIN_INPUT_FILE_PATH = "<stdin>"
2121
STDIN_FILE_NAME = "-"
2222

23+
BOOL_TRUE = frozenset({"true", "t", "1", "yes", "y"})
24+
BOOL_FALSE = frozenset({"false", "f", "0", "no", "n"})
25+
26+
DEFAULT_COMPILER_OPTS = {k.name: v for k, v in compiler.compiler_opts().items()}
27+
2328

2429
def eval_file(filename: str, ctx: compiler.CompilerContext, ns: runtime.Namespace):
2530
"""Evaluate a file with the given name into a Python module AST node."""
@@ -56,42 +61,137 @@ def bootstrap_repl(ctx: compiler.CompilerContext, which_ns: str) -> types.Module
5661
return importlib.import_module(REPL_NS)
5762

5863

64+
def _to_bool(v: Optional[str]) -> Optional[bool]:
65+
"""Coerce a string argument to a boolean value, if possible."""
66+
if v is None:
67+
return v
68+
elif v.lower() in BOOL_TRUE:
69+
return True
70+
elif v.lower() in BOOL_FALSE:
71+
return False
72+
else:
73+
raise argparse.ArgumentTypeError("Unable to coerce flag value to boolean.")
74+
75+
76+
def _set_envvar_action(
77+
var: str, parent: Type[argparse.Action] = argparse.Action
78+
) -> Type[argparse.Action]:
79+
"""Return an argparse.Action instance (deriving from `parent`) that sets the value
80+
as the default value of the environment variable `var`."""
81+
82+
class EnvVarSetterAction(parent): # type: ignore
83+
def __call__( # pylint: disable=signature-differs
84+
self,
85+
parser: argparse.ArgumentParser,
86+
namespace: argparse.Namespace,
87+
values: Any,
88+
option_string: str,
89+
):
90+
os.environ.setdefault(var, str(values))
91+
92+
return EnvVarSetterAction
93+
94+
5995
def _add_compiler_arg_group(parser: argparse.ArgumentParser) -> None:
60-
group = parser.add_argument_group("compiler arguments")
96+
group = parser.add_argument_group(
97+
"compiler arguments",
98+
description=(
99+
"The compiler arguments below can be used to tweak warnings emitted by the "
100+
"compiler during compilation and in some cases, tweak emitted code. Note "
101+
"that Basilisp, like Python, aggressively caches compiled namespaces so "
102+
"you may need to disable namespace caching or modify your file to see the "
103+
"compiler argument changes take effect."
104+
),
105+
)
61106
group.add_argument(
62107
"--warn-on-shadowed-name",
63-
action="store_const",
64-
const=True,
65-
default=os.getenv("BASILISP_WARN_ON_SHADOWED_NAME"),
66-
help="if provided, emit warnings if a local name is shadowed by another local name",
108+
action="store",
109+
nargs="?",
110+
const=os.getenv("BASILISP_WARN_ON_SHADOWED_NAME"),
111+
type=_to_bool,
112+
help=(
113+
"if true, emit warnings if a local name is shadowed by another local "
114+
"name (env: BASILISP_WARN_ON_SHADOWED_NAME; default: "
115+
f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-name']})"
116+
),
67117
)
68118
group.add_argument(
69119
"--warn-on-shadowed-var",
70-
action="store_const",
71-
const=True,
72-
default=os.getenv("BASILISP_WARN_ON_SHADOWED_VAR"),
73-
help="if provided, emit warnings if a Var name is shadowed by a local name",
120+
action="store",
121+
nargs="?",
122+
const=os.getenv("BASILISP_WARN_ON_SHADOWED_VAR"),
123+
type=_to_bool,
124+
help=(
125+
"if true, emit warnings if a Var name is shadowed by a local name "
126+
"(env: BASILISP_WARN_ON_SHADOWED_VAR; default: "
127+
f"{DEFAULT_COMPILER_OPTS['warn-on-shadowed-var']})"
128+
),
74129
)
75130
group.add_argument(
76131
"--warn-on-unused-names",
77-
action="store_const",
78-
const=True,
79-
default=os.getenv("BASILISP_WARN_ON_UNUSED_NAMES"),
80-
help="if provided, emit warnings if a local name is bound and unused",
132+
action="store",
133+
nargs="?",
134+
const=os.getenv("BASILISP_WARN_ON_UNUSED_NAMES"),
135+
type=_to_bool,
136+
help=(
137+
"if true, emit warnings if a local name is bound and unused "
138+
"(env: BASILISP_WARN_ON_UNUSED_NAMES; default: "
139+
f"{DEFAULT_COMPILER_OPTS['warn-on-unused-names']})"
140+
),
141+
)
142+
group.add_argument(
143+
"--warn-on-non-dynamic-set",
144+
action="store",
145+
nargs="?",
146+
const=os.getenv("BASILISP_WARN_ON_NON_DYNAMIC_SET"),
147+
type=_to_bool,
148+
help=(
149+
"if true, emit warnings if the compiler detects an attempt to set! "
150+
"a Var which is not marked as ^:dynamic (env: "
151+
"BASILISP_WARN_ON_NON_DYNAMIC_SET; default: "
152+
f"{DEFAULT_COMPILER_OPTS['warn-on-non-dynamic-set']})"
153+
),
81154
)
82155
group.add_argument(
83156
"--use-var-indirection",
84-
action="store_const",
85-
const=True,
86-
default=os.getenv("BASILISP_USE_VAR_INDIRECTION"),
87-
help="if provided, all Var accesses will be performed via Var indirection",
157+
action="store",
158+
nargs="?",
159+
const=os.getenv("BASILISP_USE_VAR_INDIRECTION"),
160+
type=_to_bool,
161+
help=(
162+
"if true, all Var accesses will be performed via Var indirection "
163+
"(env: BASILISP_USE_VAR_INDIRECTION; default: "
164+
f"{DEFAULT_COMPILER_OPTS['use-var-indirection']})"
165+
),
88166
)
89167
group.add_argument(
90168
"--warn-on-var-indirection",
91-
action="store_const",
169+
action="store",
170+
nargs="?",
171+
const=os.getenv("BASILISP_WARN_ON_VAR_INDIRECTION"),
172+
type=_to_bool,
173+
help=(
174+
"if true, emit warnings if a Var reference cannot be direct linked "
175+
"(env: BASILISP_WARN_ON_VAR_INDIRECTION; default: "
176+
f"{DEFAULT_COMPILER_OPTS['warn-on-var-indirection']})"
177+
),
178+
)
179+
180+
181+
def _add_debug_arg_group(parser: argparse.ArgumentParser) -> None:
182+
group = parser.add_argument_group("debug options")
183+
group.add_argument(
184+
"--disable-ns-cache",
185+
action=_set_envvar_action(
186+
"BASILISP_DO_NOT_CACHE_NAMESPACES", parent=argparse._StoreAction
187+
),
188+
nargs="?",
92189
const=True,
93-
default=os.getenv("BASILISP_WARN_ON_VAR_INDIRECTION"),
94-
help="if provided, emit warnings if a Var reference cannot be direct linked",
190+
type=_to_bool,
191+
help=(
192+
"if true, disable attempting to load cached namespaces "
193+
"(env: BASILISP_DO_NOT_CACHE_NAMESPACES; default: false)"
194+
),
95195
)
96196

97197

@@ -131,41 +231,71 @@ def repl(
131231
)
132232
basilisp.init(opts)
133233
ctx = compiler.CompilerContext(filename=REPL_INPUT_FILE_PATH, opts=opts)
134-
repl_module = bootstrap_repl(ctx, args.default_ns)
135-
ns_var = runtime.set_current_ns(args.default_ns)
136234
prompter = get_prompter()
137235
eof = object()
138-
while True:
139-
ns: runtime.Namespace = ns_var.value
140-
try:
141-
lsrc = prompter.prompt(f"{ns.name}=> ")
142-
except EOFError:
143-
break
144-
except KeyboardInterrupt: # pragma: no cover
145-
print("")
146-
continue
147-
148-
if len(lsrc) == 0:
149-
continue
150-
151-
try:
152-
result = eval_str(lsrc, ctx, ns, eof)
153-
if result is eof: # pragma: no cover
236+
237+
# Bind user-settable dynamic Vars to their existing value to allow users to
238+
# conveniently (set! *var* val) at the REPL without needing `binding`.
239+
with runtime.bindings(
240+
{
241+
var: var.value
242+
for var in map(
243+
lambda name: runtime.Var.find_safe(
244+
sym.symbol(name, ns=runtime.CORE_NS)
245+
),
246+
[
247+
"*e",
248+
"*1",
249+
"*2",
250+
"*3",
251+
"*assert*",
252+
"*data-readers*",
253+
"*resolver*",
254+
runtime.PRINT_DUP_VAR_NAME,
255+
runtime.PRINT_LEVEL_VAR_NAME,
256+
runtime.PRINT_READABLY_VAR_NAME,
257+
runtime.PRINT_LEVEL_VAR_NAME,
258+
runtime.PRINT_META_VAR_NAME,
259+
],
260+
)
261+
}
262+
):
263+
repl_module = bootstrap_repl(ctx, args.default_ns)
264+
ns_var = runtime.set_current_ns(args.default_ns)
265+
266+
while True:
267+
ns: runtime.Namespace = ns_var.value
268+
try:
269+
lsrc = prompter.prompt(f"{ns.name}=> ")
270+
except EOFError:
271+
break
272+
except KeyboardInterrupt: # pragma: no cover
273+
print("")
274+
continue
275+
276+
if len(lsrc) == 0:
277+
continue
278+
279+
try:
280+
result = eval_str(lsrc, ctx, ns, eof)
281+
if result is eof: # pragma: no cover
282+
continue
283+
prompter.print(runtime.lrepr(result))
284+
repl_module.mark_repl_result(result) # type: ignore
285+
except reader.SyntaxError as e:
286+
traceback.print_exception(reader.SyntaxError, e, e.__traceback__)
287+
repl_module.mark_exception(e) # type: ignore
288+
continue
289+
except compiler.CompilerException as e:
290+
traceback.print_exception(
291+
compiler.CompilerException, e, e.__traceback__
292+
)
293+
repl_module.mark_exception(e) # type: ignore
294+
continue
295+
except Exception as e:
296+
traceback.print_exception(Exception, e, e.__traceback__)
297+
repl_module.mark_exception(e) # type: ignore
154298
continue
155-
prompter.print(runtime.lrepr(result))
156-
repl_module.mark_repl_result(result) # type: ignore
157-
except reader.SyntaxError as e:
158-
traceback.print_exception(reader.SyntaxError, e, e.__traceback__)
159-
repl_module.mark_exception(e) # type: ignore
160-
continue
161-
except compiler.CompilerException as e:
162-
traceback.print_exception(compiler.CompilerException, e, e.__traceback__)
163-
repl_module.mark_exception(e) # type: ignore
164-
continue
165-
except Exception as e:
166-
traceback.print_exception(Exception, e, e.__traceback__)
167-
repl_module.mark_exception(e) # type: ignore
168-
continue
169299

170300

171301
@_subcommand(
@@ -181,6 +311,7 @@ def _add_repl_subcommand(parser: argparse.ArgumentParser) -> None:
181311
help="default namespace to use for the REPL",
182312
)
183313
_add_compiler_arg_group(parser)
314+
_add_debug_arg_group(parser)
184315

185316

186317
def run(
@@ -248,6 +379,7 @@ def _add_run_subcommand(parser: argparse.ArgumentParser):
248379
"--in-ns", default=runtime.REPL_DEFAULT_NS, help="namespace to use for the code"
249380
)
250381
_add_compiler_arg_group(parser)
382+
_add_debug_arg_group(parser)
251383

252384

253385
def test(parser: argparse.ArgumentParser, args: argparse.Namespace): # pragma: no cover

src/basilisp/importer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ def exec_module(self, module):
330330

331331
# Check if a valid, cached version of this Basilisp namespace exists and, if so,
332332
# load it and bypass the expensive compilation process below.
333-
if os.getenv(_NO_CACHE_ENVVAR, None) == "true":
333+
if os.getenv(_NO_CACHE_ENVVAR, "").lower() == "true":
334334
self._exec_module(fullname, spec.loader_state, path_stats, ns)
335335
else:
336336
try:

src/basilisp/lang/compiler/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from basilisp.lang import map as lmap
1010
from basilisp.lang import runtime as runtime
1111
from basilisp.lang.compiler.analyzer import ( # noqa
12+
WARN_ON_NON_DYNAMIC_SET,
1213
WARN_ON_SHADOWED_NAME,
1314
WARN_ON_SHADOWED_VAR,
1415
WARN_ON_UNUSED_NAMES,
@@ -69,10 +70,11 @@ def py_ast_optimizer(self) -> PythonASTOptimizer:
6970
return self._optimizer
7071

7172

72-
def compiler_opts(
73+
def compiler_opts( # pylint: disable=too-many-arguments
7374
warn_on_shadowed_name: Optional[bool] = None,
7475
warn_on_shadowed_var: Optional[bool] = None,
7576
warn_on_unused_names: Optional[bool] = None,
77+
warn_on_non_dynamic_set: Optional[bool] = None,
7678
use_var_indirection: Optional[bool] = None,
7779
warn_on_var_indirection: Optional[bool] = None,
7880
) -> CompilerOpts:
@@ -83,6 +85,7 @@ def compiler_opts(
8385
WARN_ON_SHADOWED_NAME: warn_on_shadowed_name or False,
8486
WARN_ON_SHADOWED_VAR: warn_on_shadowed_var or False,
8587
WARN_ON_UNUSED_NAMES: warn_on_unused_names or True,
88+
WARN_ON_NON_DYNAMIC_SET: warn_on_non_dynamic_set or True,
8689
# Generator options
8790
USE_VAR_INDIRECTION: use_var_indirection or False,
8891
WARN_ON_VAR_INDIRECTION: warn_on_var_indirection or True,

0 commit comments

Comments
 (0)