Skip to content

Commit 16c58d8

Browse files
committed
fix: don't allow lambdas to leak captures
When lambdas close over some external environment variable, if that variable is a linear value, their environment becomes the owner of the captured value and the value will be freed with the environment. If the lambda moves this variable out of its own scope, however, e.g. by returning it in its body, both the caller and the lambda environment will wind up owning the value, resulting in double frees. For now, we prevent this scenario by simply disallowing functions to leak variables captured from another scope. Doing so is now an error. Of course, one can still copy these values without issue. We achieve this through set operations. If the memory state loses the fake deleters for the set of a lambdas captured bindings after its body is evaluated, we've encountered an ownership leak, and report an error. ```clojure (defn example [] (let [capture @"" lambda (fn [] capture)])) ;; this is now an error (defn example [] (let [capture @"" lambda (fn [] @&capture)])) ;; this is still OK ``` fixes carp-lang#1040
1 parent 106bcaa commit 16c58d8

File tree

4 files changed

+27
-3
lines changed

4 files changed

+27
-3
lines changed

Diff for: src/Info.hs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Info
1010
freshVar,
1111
machineReadableInfo,
1212
makeTypeVariableNameFromInfo,
13+
deleterVar,
1314
setDeletersOnInfo,
1415
addDeletersToInfo,
1516
uniqueDeleter,

Diff for: src/Memory.hs

+18-3
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ manageMemory typeEnv globalEnv root =
111111
case lst of
112112
[defn@(XObj (Defn maybeCaptures) _ _), nameSymbol@(XObj (Sym _ _) _ _), args@(XObj (Arr argList) _ _), body] ->
113113
let captures = maybe [] Set.toList maybeCaptures
114+
captureDeleters = Set.fromList (map (FakeDeleter . getName) captures)
114115
in do
115116
mapM_ (manage typeEnv globalEnv) argList
116117
-- Add the captured variables (if any, only happens in lifted lambdas) as fake deleters
@@ -129,10 +130,11 @@ manageMemory typeEnv globalEnv root =
129130
mapM_ (addToLifetimesMappingsIfRef False) captures -- For captured variables inside of lifted lambdas
130131
visitedBody <- visit body
131132
result <- unmanage typeEnv globalEnv body
133+
capturesRetained <- assertOwnershipRetained captureDeleters xobj
132134
whenRightReturn result $
133-
do
134-
okBody <- visitedBody
135-
Right (XObj (Lst [defn, nameSymbol, args, okBody]) i t)
135+
do capturesRetained -- if any captures are given away in the body, it's an error
136+
okBody <- visitedBody
137+
Right (XObj (Lst [defn, nameSymbol, args, okBody]) i t)
136138

137139
-- Fn / λ (Lambda)
138140
[fn@(XObj (Fn _ captures) _ _), args@(XObj (Arr _) _ _), body] ->
@@ -513,6 +515,19 @@ unmanage typeEnv globalEnv xobj =
513515
tooMany -> error ("Too many variables with the same name in set: " ++ show tooMany)
514516
else pure (Right ())
515517

518+
-- | Assert that the current memory state retains ownership over a set of nodes.
519+
--
520+
-- If the provided set of deleters is not present in the memory state at the
521+
-- point at which this is called, the state has given up ownership of one or
522+
-- more of the values in the set.
523+
assertOwnershipRetained :: Set.Set Deleter -> XObj -> State MemState (Either TypeError ())
524+
assertOwnershipRetained deleters xobj =
525+
do MemState deleters' _ _ <- get
526+
if (deleters `Set.isSubsetOf` deleters')
527+
then pure (Right ())
528+
else let leaks = map deleterVar (Set.toList (deleters Set.\\ deleters'))
529+
in pure (Left (FunctionLeaksCapture leaks xobj))
530+
516531
-- | A combination of `manage` and `unmanage`.
517532
transferOwnership :: TypeEnv -> Env -> XObj -> XObj -> State MemState (Either TypeError ())
518533
transferOwnership typeEnv globalEnv from to =

Diff for: src/Set.hs

+3
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,6 @@ size (Set s) = S.size s
5454

5555
map :: Ord b => (a -> b) -> Set a -> Set b
5656
map f (Set s) = Set $ S.map f s
57+
58+
isSubsetOf :: Ord v => Set v -> Set v -> Bool
59+
isSubsetOf (Set x) (Set y) = x `S.isSubsetOf` y

Diff for: src/TypeError.hs

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ data TypeError
6363
| FailedToAddLambdaStructToTyEnv SymPath XObj
6464
| FailedToInstantiateGenericType Ty
6565
| InvalidStructField XObj
66+
| FunctionLeaksCapture [String] XObj
6667

6768
instance Show TypeError where
6869
show (SymbolMissingType xobj env) =
@@ -325,6 +326,9 @@ instance Show TypeError where
325326
++ " to the type environment."
326327
show (FailedToInstantiateGenericType ty) =
327328
"I couldn't instantiate the generic type " ++ show ty
329+
show (FunctionLeaksCapture leaks xobj) =
330+
"The function " ++ pretty xobj ++ " gives away the captured variables: "
331+
++ joinWithComma leaks ++ ". Functions must keep ownership of variables captured from another environment."
328332

329333
machineReadableErrorStrings :: FilePathPrintLength -> TypeError -> [String]
330334
machineReadableErrorStrings fppl err =
@@ -443,6 +447,7 @@ machineReadableErrorStrings fppl err =
443447
++ pretty xobj
444448
++ " to the type environment."
445449
]
450+
e@(FunctionLeaksCapture _ xobj) -> [machineReadableInfoFromXObj fppl xobj ++ show e]
446451
_ ->
447452
[show err]
448453

0 commit comments

Comments
 (0)