Carp/test/dynamic-closures.carp
Scott Olsen 381fa0f179
fix: Closure context (#1124)
* feat: add semigroup instance for Env and Context

Adds a semigroup instance for combining Envs and Contexts--this will be
necessary to ensure closure's are evaluated under the combination of the
context captured in the closure and the current global context during
evaluation.

The semigroup instances are left biased and will prefer bindings defined
in the left context/env argument in the case of conflicts (this is in
keeping with the implementation of `union` in Data.Map, the underlying
function powering this instance).

* fix: evaluate closures under the current context

Previously, closures were evaluated only under the context that was
stored during their creation. However, this can lead to issues where
closures do not resolve bindings to their latest definitions.

This commit leverages the semigroup instance of Context to evaluate
closures under the combination of the context captured during their
creation and the broader context during their evaluation/application,
preferring the context captured in the closure when bindings conflict.

This ensures that when we apply closures their local bindings still
resolve to definitions encapsulated in the closure, while other bindings
resolve to the definitions contained in the current overarching context
(instead of the old context captured by the closure).

* fix: fix bias for context env combinations in semigroup

Previously, the semigroup instance for Context was left-biased in all
the combinations of each context's environment. However, one usually
calls this function to combine some older context with a newer context,
intending to have the older context win *only* in the case of internal
environments.

This commit changes the behavior of the semigroup instance to better
reflect this use case. When one calls:

`c <> c'`

The envs in each context are combined as follows:

- internal: If conflicts occur, prefer the bindings of the context on
  the LHS (the "older" context)
- global: If conflicts occur, prefer the bindings of the context on the
  RHS ("newer" context)
- type: If conflicts occur, prefer the bindings of the context on the
  RHS ("newer" context)

This ensures the resulting context uses the latest values in the chance
of conflicts in the global env/type env, and the older values in the
case of an internal env (a closure).

* test: add basic tests for closures

* refactor: rename test/closure -> test/dynamic-closure

Also updates the forms to test dynamic closures.
2021-01-12 22:28:51 +01:00

59 lines
1.4 KiB
Plaintext

(load "Test.carp")
(use Test)
(defdynamic x 400)
;; close over a global variable
(defdynamic closure-one
(fn [] x))
;; close over an argument
(defdynamic closure-two
(fn [x] x))
;; close over a let binding
(defdynamic closure-three
(fn [] (let [x 4] x)))
;; dynamics can close over a global variable before it is defined
(defdynamic closure-four
(fn [] y))
;; nested closures prefer closed-over internal environments
(defdynamic closure-five
(fn [] (let [x 5] (fn [] x))))
(defdynamic y 500)
(defmacro test-closure-one [] (closure-one))
(defmacro test-closure-two [] (closure-two 3))
(defmacro test-closure-three [] (closure-three))
(defmacro test-closure-four [] (closure-four))
(defmacro test-closure-five [] ((closure-five)))
;; Change the global value of x (closed over in closure-one)
(set! x 1000)
(deftest test
(assert-equal test
1000
(test-closure-one)
"closures over global variables get the global variable's latest state.")
(assert-equal test
3
(test-closure-two)
"closures with arguments use argument values")
(assert-equal test
4
(test-closure-three)
"closures with let bindings prefer let bindings over global names")
(assert-equal test
500
(test-closure-four)
"dynamic closures can refer to global bindings delcared after the closure")
(assert-equal test
5
(test-closure-five)
"nested closures prefer closed-over bindings")
)