From 011e761010af92d62c53353749f03e62b9f0c92b Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Wed, 15 Jan 2025 12:03:04 +0000 Subject: [PATCH 01/21] mv local to stack --- jane/doc/extensions/{local => stack}/intro.md | 0 jane/doc/extensions/{local => stack}/pitfalls.md | 0 jane/doc/extensions/{local => stack}/reference.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename jane/doc/extensions/{local => stack}/intro.md (100%) rename jane/doc/extensions/{local => stack}/pitfalls.md (100%) rename jane/doc/extensions/{local => stack}/reference.md (100%) diff --git a/jane/doc/extensions/local/intro.md b/jane/doc/extensions/stack/intro.md similarity index 100% rename from jane/doc/extensions/local/intro.md rename to jane/doc/extensions/stack/intro.md diff --git a/jane/doc/extensions/local/pitfalls.md b/jane/doc/extensions/stack/pitfalls.md similarity index 100% rename from jane/doc/extensions/local/pitfalls.md rename to jane/doc/extensions/stack/pitfalls.md diff --git a/jane/doc/extensions/local/reference.md b/jane/doc/extensions/stack/reference.md similarity index 100% rename from jane/doc/extensions/local/reference.md rename to jane/doc/extensions/stack/reference.md From dcfd2404f9f7202ec7f52450e7e65a2ba89b9f7d Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 11:10:08 +0000 Subject: [PATCH 02/21] in place update --- jane/doc/extensions/stack/intro.md | 88 +++---- jane/doc/extensions/stack/pitfalls.md | 20 +- jane/doc/extensions/stack/reference.md | 321 ++++++++++++++----------- 3 files changed, 231 insertions(+), 198 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 2e86136fa69..535c7f96346 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -1,54 +1,56 @@ -# Introduction to Local Allocations +# Introduction to Stack Allocations See also the full feature [reference](reference.md) and [common pitfalls](pitfalls.md). -Instead of allocating values normally on the GC heap, local -allocations allow you to stack-allocate values using the new `local_` -keyword: +Instead of allocating values normally on the GC heap, you can stack-allocate +values using the new `stack_` keyword: ```ocaml -let local_ x = { foo; bar } in +let stack_ x = { foo; bar } in ... ``` or equivalently, by putting the keyword on the expression itself: ```ocaml -let x = local_ { foo; bar } in +let x = stack_ { foo; bar } in ... ``` -These values live on a separate stack, and are popped off at the end -of the _region_. Generally, the region ends when the surrounding -function returns, although read [the reference](reference.md) for more -details. +These values live in a region, and are available until the end of the _region_. +Region is the compile-time representation of stack frame at runtime. Usually, +each function body has a region, and stack-allocated values live in the +surrouding region. Read [the reference](reference.md) for more details. This helps performance in a couple of ways: first, the same few hot cache lines are constantly reused, so the cache footprint is lower than -usual. More importantly, local allocations will never trigger a GC, +usual. More importantly, stack allocations will never trigger a GC, and so they're safe to use in low-latency code that must currently be zero-alloc. -However, for this to be safe, local allocations must genuinely be -local. Since the memory they occupy is reused quickly, we must ensure -that no dangling references to them escape. This is checked by the -type-checker, and you'll see new error messages if local values leak: +However, for this to be safe, stack-allocated values must not be used after +their region ends. This is ensured by the type-checker as follows. +Stack-allocated values will be _local_ to the region they live in, and cannot +escape their region. Heap-allocated values will be _global_ and can escape any +region. If a local value tries to escape the current region, you'll see error +messages: ```ocaml -# let local_ thing = { foo; bar } in - some_global := thing;; - ^^^^^ +let foo () = + let thing = stack_ { foo; bar } in + thing + ^^^^^ Error: This value escapes its region ``` -Most of the types of allocation that OCaml does can be locally -allocated: tuples, records, variants, closures, boxed numbers, -etc. Local allocations are also possible from C stubs, although this -requires code changes to use the new `caml_alloc_local` instead of -`caml_alloc`. A few types of allocation cannot be locally allocated, -though, including first-class modules, classes and objects, and -exceptions. The contents of mutable fields (inside `ref`s, `array`s -and mutable record fields) also cannot be locally allocated. +Most allocations in OCaml can be stack-allocated: tuples, records, variants, +closures, boxed numbers, etc. Stack allocations are also possible from C stubs, +although this requires code changes to use the new `caml_alloc_local` instead of +`caml_alloc`. A few types of allocation cannot be stack-allocated, though, +including first-class modules, classes and objects, and exceptions. The contents +of mutable fields (inside `ref`s, `array`s and mutable record fields) also +cannot be stack-allocated. Annotating `stack_` on expressions that are not +allocations is meaningless and triggers type errors. ## Local parameters @@ -56,10 +58,10 @@ and mutable record fields) also cannot be locally allocated. Generally, OCaml functions can do whatever they like with their arguments: use them, return them, capture them in closures or store them in globals, etc. This is a problem when trying to pass around -locally-allocated values, since we need to guarantee they do not +local values, since we need to guarantee they do not escape. -The remedy is that we allow the `local_` keyword to also appear on +The remedy is that we allow the `local_` keyword to appear on function parameters: ```ocaml @@ -75,8 +77,8 @@ the argument. This promise is visible in the type of f: val f : local_ 'a -> ... ``` -The function f may be equally be called with locally-allocated or -GC-heap values: the `local_` annotation places obligations only on the +The function f may be equally be called with local or +global values: the `local_` annotation places obligations only on the definition of f, not its uses. Even if you're not interested in performance benefits, local @@ -106,13 +108,13 @@ val uses_callback : f:(local_ int Foo.Table.t -> 'a) -> 'a ## Inference -The examples above use the `local_` keyword to mark local +The examples above use the `stack_` keyword to mark stack allocations. In fact, this is not necessary, and the compiler will use -local allocations by default where possible. +stack allocations by default where possible. -The only effect of the keyword on e.g. a let binding is to change the -behavior for escaping values: if the bound value looks like it escapes -and therefore cannot be locally allocated, then without the keyword +The only effect of the keyword on an allocation is to change the +behavior for escaping values: if the allocated value looks like it escapes +and therefore cannot be stack-allocated, then without the keyword the compiler will allocate this value on the GC heap as usual, while with the keyword it will instead report an error. @@ -128,15 +130,15 @@ mark the local parameter in the other module's mli. ## More control There are a number of other features that allow more precise control -over which values are locally allocated, including: +over which values are stack-allocated, including: - - **Local closures** + - **Stack-allocated closures** ```ocaml - let local_ f a b c = ... + let stack_ f a b c = ... ``` - defines a function `f` whose closure is itself locally allocated. + defines a function `f` whose closure is itself stack-allocated. - **Local-returning functions** @@ -145,7 +147,7 @@ over which values are locally allocated, including: ... ``` - defines a function `f` which returns local allocations into its + defines a function `f` which returns local values into its caller's region. - **Global fields** @@ -154,8 +156,8 @@ over which values are locally allocated, including: type 'a t = { global_ g : 'a } ``` - defines a record type `t` whose `g` field is always known to be on - the GC heap (and may therefore freely escape regions), even though - the record itself may be locally allocated. + defines a record type `t` whose `g` field is always known to be global + (and thus on the GC heap and may freely escape regions), even though + the record itself may be `local`. For more details, read [the reference](./reference.md). diff --git a/jane/doc/extensions/stack/pitfalls.md b/jane/doc/extensions/stack/pitfalls.md index d833556d810..4baca176894 100644 --- a/jane/doc/extensions/stack/pitfalls.md +++ b/jane/doc/extensions/stack/pitfalls.md @@ -1,19 +1,19 @@ -# Some Pitfalls of Local Allocations +# Some Pitfalls of Stack Allocations This document outlines some common pitfalls that may come up when -trying out local allocations in a new code base, as well as some +trying out stack allocations in a new code base, as well as some suggested workarounds. Over time, this list may grow (as experience discovers new things that go wrong) or shrink (as we deploy new compiler versions that ameliorate some issues). -If you want an introduction to local allocations, see the [introduction](intro.md). +If you want an introduction to stack allocations, see the [introduction](intro.md). ## Tail calls Many OCaml functions just happen to end in a tail call, even those that are not intentionally tail-recursive. To preserve the constant-space property of tail calls, the compiler applies special -rules around local allocations in tail calls (see [the +rules around locality in tail calls (see [the reference](./reference.md)). If this causes a problem for calls that just happen to be in tail @@ -41,7 +41,7 @@ after `func` returns. ## Partial applications with local parameters -To enable the use of local allocations with higher-order functions, a +To enable the use of stack allocations with higher-order functions, a necessary step is to add local annotations to function types, particularly those of higher-order functions. For instance, an unlabeled `iter` function may become: @@ -50,12 +50,12 @@ unlabeled `iter` function may become: val iter : local_ ('a -> unit) -> 'a t -> unit ``` -thus allowing locally-allocated closures to be used as the first +thus allowing stack-allocated closures to be used as the first parameter. However, this is unfortunately not an entirely backwards-compatible change. The problem is that partial applications of `iter` functions -with the new type are themselves locally allocated, because they close +with the new type are themselves `local`, because they close over the possibly-local `f`. This means in particular that partial applications will no longer be accepted as module-level definitions: @@ -84,9 +84,9 @@ parameter of functions. ## Typing of (@@) and (|>) -The typechecking of (@@) and (|>) changed slightly with the local -allocations typechecker, in order to allow them to work with both -local and nonlocal arguments. The major difference is that: +The type-checking of (@@) and (|>) changed slightly with locality, +in order to allow them to work with both +local and global arguments. The major difference is that: f x @@ y y |> f x diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 20bca9fb26a..b673e68ab6b 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -1,61 +1,75 @@ -# Local Allocations Reference +# Stack Allocations Reference -The goal of this document is to be a reasonably complete reference to local +The goal of this document is to be a reasonably complete reference to stack allocations in OCaml. For a gentler introduction, see [the introduction](intro.md). -The local allocations language extension allows the compiler to -locally allocate some values, placing them on a stack rather than the +The stack allocations language extension allows the compiler to +stack-allocate some values, placing them on a stack rather than the garbage collected heap. Instead of waiting for the next GC, the memory -used by locally allocated values is reclaimed when their _region_ (see -below) ends, and can be immediately reused. Whether the compiler -locally allocates certain values is controlled using a new keyword -currently spelled `local_`, whose effects in expressions, patterns and -types are explained below. +used by stack-allocated values is reclaimed when their stack frame +is reclaimed, and can be immediately reused. Whether the compiler +stack-allocates certain values is controlled or inferred from new keywords +`stack_` and `local_`, whose effects are explained below. -## Local expressions and allocation +## Stack allocation and local expressions -The `local_` keyword may be placed on an expression to indicate that -allocations in that expression should be locally allocated: +The `stack_` keyword may be placed on an allocation to indicate that +it should be stack-allocated: ```ocaml -let abc = local_ [a; b; c] in +let abc = stack_ [a; b; c] in ... ``` -Here, the three cons cells of the list `[a; b; c]` will all be locally -allocated. +Here, the three cons cells of the list `[a; b; c]` will all be stack-allocated. -Equivalently, the keyword `local_` may precede the pattern in a `let`: +Equivalently, the keyword `stack_` may precede the bound variable in a `let`: ```ocaml -let local_ abc = [a; b; c] in +let stack_ abc = [a; b; c] in ... ``` -Locally allocated values may reference global (that is, GC-allocated or -constant) values, but global values may not reference local ones. In the -example above, any or all of `a`, `b` and `c` may themselves be locally -allocated. +Placing `stack_` on an expression that is not an allocation is meaningless and +causes type error. -It is valid for an expression annotated `local_` to still yield a global value. -For instance, if there is a global `x : int list` in scope, then this is -allowed: + + +Stack-allocated values are _local_, and may reference _global_ (that is, +GC-allocated) values, but _global_ values may not reference _local_ ones. +In the example above, any or all of `a`, `b` and `c` may themselves be stack-allocated. + +Global values can be weakened to local, effectively "forgetting" that a +value can escape regions. For instance, if there is a global `x : int list` in +scope, then this is allowed: + +```ocaml +let l = if n > 0 then stack_ (n :: x) else x in +... +``` + +Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. +However, if `n <= 0`, then `l` will be `x`, which is global. The later is +implicitly weakened to local and joins with the other branch, making the whole +expression local. + +Consider another example: ```ocaml let l = local_ if n > 0 then n :: x else x in ... ``` -Here, if `n > 0`, then `l` will be a locally-allocated cons cell. However, if -`n <= 0`, then `l` will be `x`, which is global. In other words, the `local_` -keyword on an expression permits but does not oblige that expression to locally -allocate its result. +The `local_` annotation forces `l` to be `local`, which prevents `l` from +escaping the current region. As a result, the compiler might optimize `n :: x` +to be stack-allocated in the current region. However, this is not to be relied +upon - you should always use `stack_` to ensure stack allocation. -Most OCaml types can be locally allocated, including records, variants, +Most OCaml types can be stack-allocated, including records, variants, polymorphic variants, closures, boxed numbers and strings. However, certain -values cannot be locally allocated, and will always be on the GC heap, +values cannot be stack-allocated, and will always be on the GC heap, including: - Modules (including first-class modules) @@ -67,17 +81,18 @@ including: - Classes and objects In addition, any value that is to be put into a mutable field (for example -inside a `ref`, an `array` or a mutable record) cannot be locally allocated. -Should you need to put a locally allocated value into one of these places, -you may want to check out [`ppx_globalize`](https://github.com/janestreet/ppx_globalize). +inside a `ref`, an `array` or a mutable record) must be global and thus cannot +be stack-allocated. Should you need to put a local value into one of these +places, you may want to check out +[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). ## Inference -In fact, the allocations of the examples above will be locally -allocated even without the `local_` keyword, if it is safe to do +In fact, the allocations of the examples above will be on +stack even without the `stack_` keyword, if it is safe to do so. The presence of the keyword on an expression only affects what happens if the value escapes (e.g. is stored into a global hash table) -and therefore cannot be locally allocated. With the keyword, an error +and therefore cannot be stack-allocated. With the keyword, an error will be reported, while without the keyword the allocations will occur on the GC heap as usual. @@ -89,7 +104,7 @@ choose `global` for anything that can be accessed from another file. Local annotations (or the lack thereof) in the mli don't affect inference within the ml. In the below example, the `~foo` parameter is inferred to -be local internally to `A`, so `foo:(Some x)` can be constructed locally. +be local internally to `a.ml`, so `foo:(Some x)` can be stack-allocated. ```ocaml (* in a.mli *) @@ -106,7 +121,7 @@ let f2 x = f1 ~foo:(Some x) (* [Some x] is stack allocated *) note; we can remove this comment when the note is resolved. --> However, a missing mli *does* affect inference within the ml. As a conservative -rule of thumb, function arguments in an mli-less file will be heap-allocated +rule of thumb, function arguments in an mli-less file will default to global unless the function parameter or argument is annotated with `local_`. This is due to an implementation detail of the type-checker and is not fundamental, but for now, it's yet another reason to prefer writing mlis. @@ -119,35 +134,39 @@ let f2 x = f1 ~foo:(Some x) (* [Some x] is heap allocated *) ## Regions -Every local allocation takes places inside a _region_, which is a block of code -(usually a function body, but see below). At the end of a region, all of its -local allocations are freed. +Every stack allocation takes places inside a stack frame, and is freed when the +stack frame is freed. Correspondingly, every OCaml value belongs to a _region_, +which is a block of code (usually a function body, but see below), and is not +available out of the region. We say the value is `local` to the region. -Regions may nest, for instance when one function calls another. Local -allocations always occur in the innermost (most recent) region. +Regions may nest, for instance when one function calls another. Stack allocaiton +always gives a value that is `local` to the current region. `global` values are +in the outermost region that represents the GC heap. We say that a value _escapes_ a region if it is still referenced beyond the end -of that region. The job of the type-checker is to ensure that locally allocated -values do not escape the region they were allocated in. +of that region. The job of the type-checker is to ensure that values do not +escape the region that they belong to. Since stack-allocated values belong to +the region representing the stack frame containing the allocation, they are +ensured to be never referenced after their stack frame is freed. -"Region" is a wider concept than "scope", and locally-allocated variables can +"Region" is a wider concept than "scope", and stack-allocated variables can outlive their scope. For example: ```ocaml let f () = - let local_ counter = - let local_ r = ref 42 in + let stack_ counter = + let stack_ r = ref 42 in incr r; r in ... ``` -The locally-allocated reference `r` is allocated inside the definition of +The stack-allocated reference `r` is allocated inside the definition of `counter`. This value outlives the scope of `r` (it is bound to the variable `counter` and may later be used in the code marked `...`). However, the -type-checker ensures that it does not outlive the region in which it is -allocated, which is the entire body of `f`. +type-checker ensures that it does not outlive the region that it belongs to, +which is the entire body of `f`. As well as function bodies, a region is also placed around: @@ -156,15 +175,15 @@ As well as function bodies, a region is also placed around: - Module bindings (`let x = ...` at module level, including in sub-modules) Module bindings are wrapped in regions to enforce the rule (as mentioned above) -that modules never contain locally-allocated values. +that modules never contain `local` values. Additionally, it is possible to write functions that do *not* have a region around their body, which is useful to write functions that return -locally-allocated values. See "Use exclave_ to return a local value" below. +stack-allocated values. See "Use exclave_ to return a local value" below. ### Runtime behavior -At runtime, local allocations do not allocate on the C stack, but on a +At runtime, stack allocations do not take place on the C stack, but on a separately-allocated stack that follows the same layout as the OCaml minor heap. In particular, this allows local-returning functions without the need to copy returned values. @@ -175,51 +194,53 @@ value. ### Variables and regions -To spot escaping local allocations, the type checker internally tracks whether +To spot escaping `local` values, the type checker internally tracks whether each variable is: + + - **Global**: must be a global value. These variables are allowed to freely cross region boundaries, as normal OCaml values. - **Local**: may be a locally-allocated value. These variables are restricted from crossing region boundaries. -As described above, whether a given variable is global or local is inferred by -the type-checker, although the `local_` keyword may be used to specify it. +As described above, whether a given variable is global or local (and hence +whether or not optimized to stack allocation) is inferred by +the type-checker, although the `stack_` keyword may be used to specify it. -Additionally, local variables are further subdivided into two cases: +Additionally, `local` variables are further subdivided into two cases: - - **Outer-region local**: may be a locally-allocated value, but only from an - outer region and not from the current one. + - **Outer-region local**: belongs to an outer region, not the current region. - - **Any-region local**: may be a locally-allocated value, even one allocated - during the current region. + - **Any-region local**: belongs to an unknown region, potentially the current + region. For instance: ```ocaml let f () = - let local_ outer = ref 42 in + let stack_ outer = ref 42 in let g () = - let local_ inner = ref 42 in + let stack_ inner = ref 42 in ?? in ... ``` At the point marked `??` inside `g`, both `outer` and `inner` are -locally-allocated values. However, only `inner` is any-region local, having been -allocated in `g`'s region. The value `outer` is instead outer-region local: it -is locally allocated but from a region other than `g`'s own. +stack-allocated values and thus `local`. However, only `inner` is any-region +local, having been allocated in `g`'s region. The value `outer` is instead +outer-region local: it is stack-allocated but from a region outer than `g`'s +own. So, if we replace `??` with `inner`, we see an error: Error: This local value escapes its region However, if we replace `??` with `outer`, the compiler will accept it: the -value `outer`, while locally allocated, was definitely not locally allocated -_during g_, and there is therefore no problem allowing it to escape `g`'s -region. +value `outer`, while being `local`, was definitely not `local` to the region of +`g`, and there is therefore no problem allowing it to escape `g`'s region. (This is quite subtle, and there is an additional wrinkle: how does the compiler know that it is safe to still refer to `outer` from within the closure @@ -236,16 +257,20 @@ positions, leading to four distinct types of function: a -> local_ b local_ a -> local_ b +In all cases, the `local_` annotation means "local to the caller's region" +, or equivalently "outer-region local to the callee's region" if the callee has +a region. + In argument positions, `local_` indicates that the function may be passed -locally-allocated values. As always, the local_ keyword does not *require* -a locally-allocated value, and you may pass global values to such functions. In +`local` values. As always, the `local_` keyword does not *require* +a stack-allocated value, and you may pass `global` values to such functions. In effect, a function of type `local_ a -> b` is a function accepting `a` and returning `b` that promises not to capture any reference to its argument. -In return positions, `local_` indicates that the function may return -locally-allocated values. A function of type `local_ a -> local_ b` promises -not to capture any reference to its argument except possibly in its return -value. +In return positions, `local_` indicates that the function may return values that +are `local` (See "Use `exclave_` to return a local value" below). A function of +type `local_ a -> local_ b` promises not to capture any reference to its +argument except possibly in its return value. A function with a local argument can be defined by annotating the argument as `local_`: @@ -275,43 +300,43 @@ In the above, `f1` returns a global `int list`, while `f2` returns a local one. function's region, because the value `x` is known to come from outside that region. -In contrast, `f3` is an error. The value `42 :: x` must be locally allocated (as -it refers to a local value `x`), and it is locally allocated from within the -region of `f3`. When this region ends, the any-region local value `42 :: x` is -not allowed to escape it. +In contrast, `f3` is an error. The value `42 :: x` refers to a local value `x`, +which means it cannot be `global`. Therefore, it must be stack-allocated, and it + is allocated within region of `f3`. When this region ends, the any-region local +value `42 :: x` is not allowed to escape it. -It is possible to write functions like `f3` that return locally-allocated +It is possible to write functions like `f3` that return stack-allocated values, but this requires explicit annotation, as it would otherwise be easy to do by mistake. See "Use exclave_ to return a local value" below. Like local variables, inference can determine whether function arguments are -local. However, note that for arguments of exported functions to be local, the +`local`. However, note that for arguments of exported functions to be local, the `local_` keyword must appear in their declarations in the corresponding `.mli` file. ## Closures -Like most other values, closures can be locally allocated. In particular, this -happens when a closure closes over local values from an outer scope: since -global values cannot refer to local values, all such closures _must_ be locally -allocated. +Like most other values, closures can be stack-allocated. In particular, this +happens when a closure closes over `local` values: since `global` values cannot +refer to `local` values, all such closures cannot be `global` and _must_ be +stack-allocated. Consider again the example from "Variables and regions" above: ```ocaml let f () = - let local_ outer = ref 42 in + let stack_ outer = ref 42 in let g () = - let local_ inner = ref 42 in + let stack_ inner = ref 42 in outer in ... ``` Here, since `g` refers to the local value `outer`, the closure `g` must itself -be locally allocated. (As always, this is deduced by inference, and an explicit -`local_` annotation on `g` is not needed). +be stack-allocated. (As always, this is deduced by inference, and an explicit +`stack_` annotation on `g` is not needed). This then means that `g` is not allowed to escape its region, i.e. the body of `f`. `f` may call `g` but may not return the closure. This guarantees that `g` @@ -324,7 +349,7 @@ following function for computing the length of a list: ```ocaml let length xs = - let local_ count = ref 0 in + let stack_ count = ref 0 in List.iter xs ~f:(fun () -> incr count); !count ``` @@ -375,7 +400,7 @@ by `iter` not to capture `f`, while the second is a requirement by ## Tail calls Usually, a function's region lasts for the entire body of that function, -cleaning up local allocations at the very end. This story gets more complicated +and local values are available until at the very end. This story gets more complicated if the function ends in a tail call, however, as such functions need to clean up their stack frame before the tail call in order to ensure that tail-recursive loops use only constant space. @@ -392,7 +417,7 @@ values may not be passed to tail calls: ```ocaml let f1 () = - let local_ r = ref 42 in + let stack_ r = ref 42 in some_func r ^ Error: This local value escapes its region @@ -403,7 +428,7 @@ and any-region local closures may not be tail-called: ```ocaml let f2 () = - let local_ g () = 42 in + let stack_ g () = 42 in g () ^ Error: This local value escapes its region @@ -415,12 +440,12 @@ resolved by moving the call so that it is not syntactically a tail call: ```ocaml let f1 () = - let local_ r = ref 42 in + let stack_ r = ref 42 in let res = some_func r in res let f2 () = - let local_ g () = 42 in + let stack_ g () = 42 in let res = g () in res ``` @@ -430,16 +455,16 @@ prevents it from being a tail call: ```ocaml let f1 () = - let local_ r = ref 42 in + let stack_ r = ref 42 in some_func r [@nontail] let f2 () = - let local_ g () = 42 in + let stack_ g () = 42 in g () [@nontail] ``` -This change means that the locally allocated values (`r` and `g`) -will not be freed until after the call has returned. +These changes makes the `local` values (`r` and `g`) stay available until after +the call has returned. Note that values which are outer-region local rather than any-region local (that is, local values that were passed into this region from outside) may be used in @@ -455,7 +480,7 @@ value `x` remains available. ## Use `exclave_` to return a local value -The region around the body of a function prevents local allocations inside that +The region around the body of a function prevents local values inside that function from escaping. Occasionally, it is useful to write a function that allocates and returns a value in the caller's region. For instance, consider this code that uses an `int ref` as a counter: @@ -469,8 +494,8 @@ let f () = ... ``` -Here, inference will detect that `counter` does not escape and will allocate -the reference locally. However, this changes if we try to abstract out +Here, inference will detect that `counter` does not escape and will stack-allocate +the reference. However, this changes if we try to abstract out `counter` to its own module: ```ocaml @@ -493,13 +518,13 @@ let f () = ... ``` -In this code, the counter will *not* be allocated locally. The reason is the +In this code, the counter will *not* be stack-allocated. The reason is the `Counter.make` function: the allocation of `ref 0` escapes the region of -`Counter.make`, and the compiler will therefore not allow it to be locally -allocated. This remains the case no matter how many `local_` annotations we +`Counter.make`, and the compiler will therefore not allow it to be +stack-allocated. This remains the case no matter how many `local_` annotations we write inside `f`: the issue is the definition of `make`, not its uses. -To allow the counter to be locally allocated, we need to make `Counter.make` end +To allow the counter to be stack-allocated, we need to make `Counter.make` end its region early so that it can allocate its return value in the caller's region. This can be done with `exclave_`: @@ -510,7 +535,7 @@ let make () = exclave_ The keyword `exclave_` terminates the current region and executes the subsequent code in the outer region. Therefore, `ref 0` is executed in `f`'s region, which -allows its local allocation. The allocation will only be cleaned up when the +allows its stack-allocation. The allocation will only be cleaned up when the region of `f` ends. ## Delaying exclaves @@ -520,7 +545,7 @@ however, has certain disadvantages. Consider the following example: ```ocaml let f (local_ x) = exclave_ - let local_ y = (complex computation on x) in + let stack_ y = (complex computation on x) in if y then None else (Some x) ``` @@ -532,17 +557,16 @@ upon the function's return, we delay `exclave_` as follows: ```ocaml let f (local_ x) = - let local_ y = (complex computation on x) in + let stack_ y = (complex computation on x) in if y then exclave_ None else exclave_ Some x ``` In this example, the function `f` has a region where the allocation for the complex computation occurs. This region is terminated by `exclave_`, releasing -all temporary allocations. Both `None` and `Some x` are considered "local" -relative to the outer region and are allowed to be returned. In summary, the +all temporary allocations. Both `None` and `Some x` are outer-region local and are allowed to be returned. In summary, the temporary allocations in the `f`'s region are promptly released, and the result -allocation in the caller's region is returned. +allocated in the caller's region is returned. Here is another example in which the stack usage can be improved asymptotically by delaying `exclave_`: @@ -585,7 +609,7 @@ Now the function uses O(1) stack space. `exclave_` terminates the current region, so local values from that region cannot be used inside `exclave_`. For example, the following code produces an -error because `x` would escape its region: +error: ```ocaml let local_ x = "hello" in exclave_ ( @@ -615,16 +639,16 @@ both global: ```ocaml let f () = - let local_ packed = (x, y) in + let stack_ packed = (x, y) in let x', y' = packed in x' ``` -Here, the `packed` values is treated as local, and the type-checker then -conservatively assumes that `x'` and `y'` may also be local (since they are +Here, the `packed` values is treated as `local`, and the type-checker then +conservatively assumes that `x'` and `y'` may also be `local` (since they are extracted from `packed`), and so cannot safely be returned. -Similarly, a variable `local_ x` of type `string list` means a local +Similarly, a `local` value of type `string list` means a local list of local strings, and none of these strings can be safely returned from a function like `f`. @@ -635,14 +659,14 @@ This can be overridden for record types, by annotating some fields with type ('a, 'b) t = { global_ foo : 'a; bar: 'b } let f () = - let local_ packed = {foo=x; bar=y} in + let stack_ packed = {foo=x; bar=y} in let {foo; bar} = packed in foo ``` Here, the `foo` field of any value of type `_ t` is always known to be global, and so can be returned from a function. When constructing such a record, the -`foo` field must therefore be a global value, so trying to fill it with a local +`foo` field must therefore be assigned a global value, so trying to fill it with a local value will result in an escape error, even if the record being constructed is itself local. @@ -652,7 +676,7 @@ In particular, by defining: type 'a t = { global_ global : 'a } [@@unboxed] ``` -then a variable `local_ x` of type `string t list` is a local list of global +then a `local` value of type `string t list` is a local list of global strings, and while the list itself cannot be returned out of a region, the `global` field of any of its elements can. For convenience, `base` provides this as the type `Modes.Global.t`. @@ -663,7 +687,7 @@ for record fields: type ('a, 'b) t = Foo of global_ 'a * 'b let f () = - let local_ packed = Foo (x, y) in + let stack_ packed = Foo (x, y) in match packed with | Foo (foo, bar) -> foo ``` @@ -738,31 +762,30 @@ local_ (a -> local_ (b -> local_ (c -> d))) -> local_ (e -> local_ (f -> g)) Note the implicit `local_` both in the returned `e -> f` closure (as described above), and also in the type of the `b -> c` argument. The propagation of -`local_` into the function argument is necessary to allow a locally-allocated +`local_` into the function argument is necessary to allow a stack-allocated function (which would have type `a -> local_ (b -> local_ (c -> d))`) to be passed as an argument. Functions are different than other types in that, because -of currying, a locally-allocated function has a different type than a -globally-allocated one. +of currying, a stack-allocated function has a different type than a +heap-allocated one. ### Currying of local closures - Suppose we are inside the definition of a function, and there is in scope a local value `counter` of type `int ref`. Then of the following two seemingly-identical definitions, the first is accepted and the second is rejected: ```ocaml -let local_ f : int -> int -> int = fun a b -> a + b + !counter in +let stack_ f : int -> int -> int = fun a b -> a + b + !counter in ... -let f : int -> int -> int = local_ fun a b -> a + b + !counter in +let f : int -> int -> int = stack_ fun a b -> a + b + !counter in ... ``` Both define a closure which accepts two integers and returns an integer. The closure must be local, since it refers to the local value `counter`. In the -former definition, the type of the function appears under the `local_` keyword, -as as described above is interpreted as: +former definition, the type of the function appears under the `stack_` keyword, +and as described above is interpreted as: ```ocaml int -> local_ (int -> int) @@ -771,7 +794,7 @@ int -> local_ (int -> int) This is the correct type for this function: if we partially apply it to a single argument, the resulting closure will still be local, as it refers to the original function which refers to `counter`. By contrast, in the latter -definition the type of the function is outside the `local_` keyword as is +definition the type of the function is outside the `stack_` keyword as is interpreted as normal as: ```ocaml @@ -784,7 +807,7 @@ case here. For this reason, this version is rejected. It would be accepted if written as follows: ```ocaml -let f : int -> local_ (int -> int) = local_ fun a b -> a + b + !counter in +let f : int -> local_ (int -> int) = stack_ fun a b -> a + b + !counter in ... ``` @@ -831,13 +854,17 @@ making all of `a`,`b` and `c` local if any of `x`, `y` and `z` are. ## Primitive definitions +Sometimes we might want a function that can be instantiated with different modes. +For example, we might want a single `id` function that can work as either of the +following: -Allocations in OCaml functions must either be local or global, as these are -compiled separately. A different option is available for `%`-primitives exported -by the stdlib, however, as these are guaranteed to be inlined at every use -site. Unlike ordinary functions, these primitives may be used to make both -local and global allocations, which is why `ref` worked for both local and -global in various examples above. +```ocaml +id : local_ 'a -> local_ a +id : 'a -> 'a +``` + +Mode polymorphism is not available yet in general, but a different option is +available for `%`-primitives. In the interface for the stdlib (and as re-exported by Base), this feature is enabled by use of the `[@local_opt]` annotation on `external` declarations. For @@ -846,13 +873,7 @@ example, we have the following: ```ocaml external id : ('a[@local_opt]) -> ('a[@local_opt]) = "%identity" ``` - -This declaration means that `id` can have either of the following types: - -```ocaml -id : local_ 'a -> local_ a -id : 'a -> 'a -``` +which achieves what we wanted above. Notice that the two `[@local_opt]`s act in unison: either both `local_`s are present or neither is. This allows for a limited form of mode-polymorphism for @@ -861,3 +882,13 @@ though, so use this feature with much caution. In the case of `id`, all is well, but if the two `[@local_opt]`s did not act in unison (that is, they varied independently), it would not be: `id : local_ 'a -> 'a` allows a local value to escape. + +Moreover, since primitives are guaranteed to be inlined at every use site, +it can have different runtime behavior (such as allocation) according to the instantiated modes. For example, `ref` is defined as + +```ocaml +external ref : 'a -> ('a ref[@local_opt]) = "%makemutable" +``` + +which would allocate the cell on GC heap or on stack, depending on the +instantiated mode. From b054bb0b48eacd0e9524e78f744aacf704a1ecb3 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 11:14:07 +0000 Subject: [PATCH 03/21] move "region" section above --- jane/doc/extensions/stack/reference.md | 194 ++++++++++++------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index b673e68ab6b..82e7c807098 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -35,103 +35,6 @@ let stack_ abc = [a; b; c] in Placing `stack_` on an expression that is not an allocation is meaningless and causes type error. - - -Stack-allocated values are _local_, and may reference _global_ (that is, -GC-allocated) values, but _global_ values may not reference _local_ ones. -In the example above, any or all of `a`, `b` and `c` may themselves be stack-allocated. - -Global values can be weakened to local, effectively "forgetting" that a -value can escape regions. For instance, if there is a global `x : int list` in -scope, then this is allowed: - -```ocaml -let l = if n > 0 then stack_ (n :: x) else x in -... -``` - -Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. -However, if `n <= 0`, then `l` will be `x`, which is global. The later is -implicitly weakened to local and joins with the other branch, making the whole -expression local. - -Consider another example: - -```ocaml -let l = local_ if n > 0 then n :: x else x in -... -``` - -The `local_` annotation forces `l` to be `local`, which prevents `l` from -escaping the current region. As a result, the compiler might optimize `n :: x` -to be stack-allocated in the current region. However, this is not to be relied -upon - you should always use `stack_` to ensure stack allocation. - -Most OCaml types can be stack-allocated, including records, variants, -polymorphic variants, closures, boxed numbers and strings. However, certain -values cannot be stack-allocated, and will always be on the GC heap, -including: - - - Modules (including first-class modules) - - - Exceptions - (Technically, values of type `exn` can be locally allocated, but only global - ones may be raised) - - - Classes and objects - -In addition, any value that is to be put into a mutable field (for example -inside a `ref`, an `array` or a mutable record) must be global and thus cannot -be stack-allocated. Should you need to put a local value into one of these -places, you may want to check out -[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). - -## Inference - -In fact, the allocations of the examples above will be on -stack even without the `stack_` keyword, if it is safe to do -so. The presence of the keyword on an expression only affects what -happens if the value escapes (e.g. is stored into a global hash table) -and therefore cannot be stack-allocated. With the keyword, an error -will be reported, while without the keyword the allocations will occur -on the GC heap as usual. - -Inference does not cross file boundaries. If local annotations subject to -inference appear in the type of a module (e.g. since they can appear in -function types, see below) then inference will resolve them according to what -appears in the `.mli`. If there is no `.mli` file, then inference will always -choose `global` for anything that can be accessed from another file. - -Local annotations (or the lack thereof) in the mli don't affect inference -within the ml. In the below example, the `~foo` parameter is inferred to -be local internally to `a.ml`, so `foo:(Some x)` can be stack-allocated. - -```ocaml -(* in a.mli *) -val f1 : foo:local_ int option -> unit -val f2 : int -> unit - -(* in a.ml *) -let f1 ~foo:_ = () -let f2 x = f1 ~foo:(Some x) (* [Some x] is stack allocated *) -``` - - -However, a missing mli *does* affect inference within the ml. As a conservative -rule of thumb, function arguments in an mli-less file will default to global -unless the function parameter or argument is annotated with `local_`. This is -due to an implementation detail of the type-checker and is not fundamental, but -for now, it's yet another reason to prefer writing mlis. - -```ocaml -(* in a.ml; a.mli is missing *) -let f1 ~foo:_ = () -let f2 x = f1 ~foo:(Some x) (* [Some x] is heap allocated *) -``` - ## Regions Every stack allocation takes places inside a stack frame, and is freed when the @@ -247,6 +150,103 @@ compiler know that it is safe to still refer to `outer` from within the closure `g`? See "Closures" below for more details) +Stack-allocated values are _local_, and may reference _global_ (that is, +GC-allocated) values, but _global_ values may not reference _local_ ones. +In the example above, any or all of `a`, `b` and `c` may themselves be stack-allocated. + +Global values can be weakened to local, effectively "forgetting" that a +value can escape regions. For instance, if there is a global `x : int list` in +scope, then this is allowed: + +```ocaml +let l = if n > 0 then stack_ (n :: x) else x in +... +``` + +Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. +However, if `n <= 0`, then `l` will be `x`, which is global. The later is +implicitly weakened to local and joins with the other branch, making the whole +expression local. + +Consider another example: + +```ocaml +let l = local_ if n > 0 then n :: x else x in +... +``` + +The `local_` annotation forces `l` to be `local`, which prevents `l` from +escaping the current region. As a result, the compiler might optimize `n :: x` +to be stack-allocated in the current region. However, this is not to be relied +upon - you should always use `stack_` to ensure stack allocation. + +Most OCaml types can be stack-allocated, including records, variants, +polymorphic variants, closures, boxed numbers and strings. However, certain +values cannot be stack-allocated, and will always be on the GC heap, +including: + + - Modules (including first-class modules) + + - Exceptions + (Technically, values of type `exn` can be locally allocated, but only global + ones may be raised) + + - Classes and objects + +In addition, any value that is to be put into a mutable field (for example +inside a `ref`, an `array` or a mutable record) must be global and thus cannot +be stack-allocated. Should you need to put a local value into one of these +places, you may want to check out +[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). + +## Inference + +In fact, the allocations of the examples above will be on +stack even without the `stack_` keyword, if it is safe to do +so. The presence of the keyword on an expression only affects what +happens if the value escapes (e.g. is stored into a global hash table) +and therefore cannot be stack-allocated. With the keyword, an error +will be reported, while without the keyword the allocations will occur +on the GC heap as usual. + +Inference does not cross file boundaries. If local annotations subject to +inference appear in the type of a module (e.g. since they can appear in +function types, see below) then inference will resolve them according to what +appears in the `.mli`. If there is no `.mli` file, then inference will always +choose `global` for anything that can be accessed from another file. + +Local annotations (or the lack thereof) in the mli don't affect inference +within the ml. In the below example, the `~foo` parameter is inferred to +be local internally to `a.ml`, so `foo:(Some x)` can be stack-allocated. + +```ocaml +(* in a.mli *) +val f1 : foo:local_ int option -> unit +val f2 : int -> unit + +(* in a.ml *) +let f1 ~foo:_ = () +let f2 x = f1 ~foo:(Some x) (* [Some x] is stack allocated *) +``` + + +However, a missing mli *does* affect inference within the ml. As a conservative +rule of thumb, function arguments in an mli-less file will default to global +unless the function parameter or argument is annotated with `local_`. This is +due to an implementation detail of the type-checker and is not fundamental, but +for now, it's yet another reason to prefer writing mlis. + +```ocaml +(* in a.ml; a.mli is missing *) +let f1 ~foo:_ = () +let f2 x = f1 ~foo:(Some x) (* [Some x] is heap allocated *) +``` + + + ## Function types and local arguments Function types now accept the `local_` keyword in both argument and return From 230b14b68df426726b80e32d91fd8cb738a194ac Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 11:59:32 +0000 Subject: [PATCH 04/21] further polish --- jane/doc/extensions/stack/reference.md | 79 +++++++++++++------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 82e7c807098..ff7363e65db 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -19,38 +19,52 @@ The `stack_` keyword may be placed on an allocation to indicate that it should be stack-allocated: ```ocaml -let abc = stack_ [a; b; c] in +let abc = stack_ (42, 24) in ... ``` -Here, the three cons cells of the list `[a; b; c]` will all be stack-allocated. +Here, the tuple cell will be stack-allocated. Equivalently, the keyword `stack_` may precede the bound variable in a `let`: ```ocaml -let stack_ abc = [a; b; c] in +let stack_ abc = (42, 24) in ... ``` Placing `stack_` on an expression that is not an allocation is meaningless and causes type error. -## Regions +### Regions Every stack allocation takes places inside a stack frame, and is freed when the -stack frame is freed. Correspondingly, every OCaml value belongs to a _region_, -which is a block of code (usually a function body, but see below), and is not -available out of the region. We say the value is `local` to the region. - -Regions may nest, for instance when one function calls another. Stack allocaiton -always gives a value that is `local` to the current region. `global` values are -in the outermost region that represents the GC heap. +stack frame is freed. For this to be safe, stack-allocated values cannot be used +after their stack frame is freed. This runtime behavior is guaranteed at +compile-time by the type checker as follows. + +Every OCaml value lives in a _region_. Usually a function body has a region, +representing the function's stack frame at runtime. A stack-allocated value +lives in the region it's allocated in. We say the value is _local_ to the region +it lives in. Regions may nest, for instance when one function calls another. +Stack allocaiton always gives a value that lives in the current region, since at +runtime one can only allocate in the current stack frame. There is an outermost +region, and values living in it are _global_. Heap-allocated values live in this +region. We say that a value _escapes_ a region if it is still referenced beyond the end -of that region. The job of the type-checker is to ensure that values do not -escape the region that they belong to. Since stack-allocated values belong to -the region representing the stack frame containing the allocation, they are -ensured to be never referenced after their stack frame is freed. +of that region. The type-checker guarantees that values do not escape the region +that they live in. Since stack-allocated values live in the region representing +the stack frame containing the allocation, they are guaranteed to be never +referenced after their stack frame is freed. + +The above guarantee means that global values can escape all regions except the +outermost one. However, we say the outermost region never ends, so values never +need to escape that region. + +Global values, being allowed to escape regions, may not reference local ones, +since that will make the local values escape regions, which breaks the +guarantee. Local values may reference global ones. + "Region" is a wider concept than "scope", and stack-allocated variables can outlive their scope. For example: @@ -68,7 +82,7 @@ let f () = The stack-allocated reference `r` is allocated inside the definition of `counter`. This value outlives the scope of `r` (it is bound to the variable `counter` and may later be used in the code marked `...`). However, the -type-checker ensures that it does not outlive the region that it belongs to, +type-checker ensures that it does not outlive the region that it lives in, which is the entire body of `f`. As well as function bodies, a region is also placed around: @@ -95,28 +109,14 @@ The beginning of a region records the stack pointer of this local stack, and the end of the region resets the stack pointer to this value. -### Variables and regions - -To spot escaping `local` values, the type checker internally tracks whether -each variable is: - - - - - **Global**: must be a global value. These variables are allowed to freely - cross region boundaries, as normal OCaml values. - - - **Local**: may be a locally-allocated value. These variables are restricted - from crossing region boundaries. - -As described above, whether a given variable is global or local (and hence -whether or not optimized to stack allocation) is inferred by -the type-checker, although the `stack_` keyword may be used to specify it. +### Nested regions +Let's further explore the idea of nested regions mentioned above. Additionally, `local` variables are further subdivided into two cases: - - **Outer-region local**: belongs to an outer region, not the current region. + - **Outer-region local**: lives in an outer region, not the current region. - - **Any-region local**: belongs to an unknown region, potentially the current + - **Any-region local**: lives in an unknown region, potentially the current region. For instance: @@ -150,9 +150,6 @@ compiler know that it is safe to still refer to `outer` from within the closure `g`? See "Closures" below for more details) -Stack-allocated values are _local_, and may reference _global_ (that is, -GC-allocated) values, but _global_ values may not reference _local_ ones. -In the example above, any or all of `a`, `b` and `c` may themselves be stack-allocated. Global values can be weakened to local, effectively "forgetting" that a value can escape regions. For instance, if there is a global `x : int list` in @@ -203,11 +200,13 @@ places, you may want to check out In fact, the allocations of the examples above will be on stack even without the `stack_` keyword, if it is safe to do -so. The presence of the keyword on an expression only affects what -happens if the value escapes (e.g. is stored into a global hash table) +so. The presence of the keyword on an allocation only affects what +happens if the allocated value escapes (e.g. is stored into a global hash table) and therefore cannot be stack-allocated. With the keyword, an error will be reported, while without the keyword the allocations will occur -on the GC heap as usual. +on the GC heap as usual. Similarly, whether a value is global or local (and +hence whether certain allocation can be on stack) is inferred by the +type-checker, although the `local_` keyword may be used to specify it. Inference does not cross file boundaries. If local annotations subject to inference appear in the type of a module (e.g. since they can appear in From 6b8b750b96f1515204e2f44f4a9c515610693e4b Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 12:01:22 +0000 Subject: [PATCH 05/21] just move things --- jane/doc/extensions/stack/reference.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index ff7363e65db..77063db8cf9 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -35,6 +35,17 @@ let stack_ abc = (42, 24) in Placing `stack_` on an expression that is not an allocation is meaningless and causes type error. +### Runtime behavior + +At runtime, stack allocations do not take place on the C stack, but on a +separately-allocated stack that follows the same layout as the OCaml +minor heap. In particular, this allows local-returning functions +(see "Use exclave_ to return a local value" below ) +without the need to copy returned values. + +The beginning of a stack frame records the stack pointer of this local stack, +and the end of the stack frame resets the stack pointer to this value. + ### Regions Every stack allocation takes places inside a stack frame, and is freed when the @@ -98,16 +109,6 @@ Additionally, it is possible to write functions that do *not* have a region around their body, which is useful to write functions that return stack-allocated values. See "Use exclave_ to return a local value" below. -### Runtime behavior - -At runtime, stack allocations do not take place on the C stack, but on a -separately-allocated stack that follows the same layout as the OCaml -minor heap. In particular, this allows local-returning functions -without the need to copy returned values. - -The beginning of a region records the stack pointer of this local -stack, and the end of the region resets the stack pointer to this -value. ### Nested regions From 627b9d1485f574c49fd46bb31fad7e8923ef865e Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 12:07:34 +0000 Subject: [PATCH 06/21] polish --- jane/doc/extensions/stack/reference.md | 43 +++++++++++++------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 77063db8cf9..9a6bc892b96 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -103,17 +103,16 @@ As well as function bodies, a region is also placed around: - Module bindings (`let x = ...` at module level, including in sub-modules) Module bindings are wrapped in regions to enforce the rule (as mentioned above) -that modules never contain `local` values. +that modules never contain local values. Additionally, it is possible to write functions that do *not* have a region around their body, which is useful to write functions that return stack-allocated values. See "Use exclave_ to return a local value" below. - - ### Nested regions -Let's further explore the idea of nested regions mentioned above. -Additionally, `local` variables are further subdivided into two cases: +Let's further explore the idea of nested regions mentioned above. Say we are in +the middle of a region, the local variables in scope are further subdivided into +two cases: - **Outer-region local**: lives in an outer region, not the current region. @@ -133,7 +132,7 @@ let f () = ``` At the point marked `??` inside `g`, both `outer` and `inner` are -stack-allocated values and thus `local`. However, only `inner` is any-region +stack-allocated values and thus local. However, only `inner` is any-region local, having been allocated in `g`'s region. The value `outer` is instead outer-region local: it is stack-allocated but from a region outer than `g`'s own. @@ -143,7 +142,7 @@ So, if we replace `??` with `inner`, we see an error: Error: This local value escapes its region However, if we replace `??` with `outer`, the compiler will accept it: the -value `outer`, while being `local`, was definitely not `local` to the region of +value `outer`, while being local, was definitely not local to the region of `g`, and there is therefore no problem allowing it to escape `g`'s region. (This is quite subtle, and there is an additional wrinkle: how does the @@ -173,7 +172,7 @@ let l = local_ if n > 0 then n :: x else x in ... ``` -The `local_` annotation forces `l` to be `local`, which prevents `l` from +The `local_` annotation forces `l` to be local, which prevents `l` from escaping the current region. As a result, the compiler might optimize `n :: x` to be stack-allocated in the current region. However, this is not to be relied upon - you should always use `stack_` to ensure stack allocation. @@ -213,7 +212,7 @@ Inference does not cross file boundaries. If local annotations subject to inference appear in the type of a module (e.g. since they can appear in function types, see below) then inference will resolve them according to what appears in the `.mli`. If there is no `.mli` file, then inference will always -choose `global` for anything that can be accessed from another file. +choose global for anything that can be accessed from another file. Local annotations (or the lack thereof) in the mli don't affect inference within the ml. In the below example, the `~foo` parameter is inferred to @@ -262,13 +261,13 @@ In all cases, the `local_` annotation means "local to the caller's region" a region. In argument positions, `local_` indicates that the function may be passed -`local` values. As always, the `local_` keyword does not *require* -a stack-allocated value, and you may pass `global` values to such functions. In +local values. As always, the `local_` keyword does not *require* +a stack-allocated value, and you may pass global values to such functions. In effect, a function of type `local_ a -> b` is a function accepting `a` and returning `b` that promises not to capture any reference to its argument. In return positions, `local_` indicates that the function may return values that -are `local` (See "Use `exclave_` to return a local value" below). A function of +are local (See "Use `exclave_` to return a local value" below). A function of type `local_ a -> local_ b` promises not to capture any reference to its argument except possibly in its return value. @@ -301,7 +300,7 @@ function's region, because the value `x` is known to come from outside that region. In contrast, `f3` is an error. The value `42 :: x` refers to a local value `x`, -which means it cannot be `global`. Therefore, it must be stack-allocated, and it +which means it cannot be global. Therefore, it must be stack-allocated, and it is allocated within region of `f3`. When this region ends, the any-region local value `42 :: x` is not allowed to escape it. @@ -310,7 +309,7 @@ values, but this requires explicit annotation, as it would otherwise be easy to do by mistake. See "Use exclave_ to return a local value" below. Like local variables, inference can determine whether function arguments are -`local`. However, note that for arguments of exported functions to be local, the +local. However, note that for arguments of exported functions to be local, the `local_` keyword must appear in their declarations in the corresponding `.mli` file. @@ -318,8 +317,8 @@ file. ## Closures Like most other values, closures can be stack-allocated. In particular, this -happens when a closure closes over `local` values: since `global` values cannot -refer to `local` values, all such closures cannot be `global` and _must_ be +happens when a closure closes over local values: since global values cannot +refer to local values, all such closures cannot be global and _must_ be stack-allocated. Consider again the example from "Variables and regions" above: @@ -463,7 +462,7 @@ let f2 () = g () [@nontail] ``` -These changes makes the `local` values (`r` and `g`) stay available until after +These changes makes the local values (`r` and `g`) stay available until after the call has returned. Note that values which are outer-region local rather than any-region local (that @@ -644,11 +643,11 @@ let f () = x' ``` -Here, the `packed` values is treated as `local`, and the type-checker then -conservatively assumes that `x'` and `y'` may also be `local` (since they are +Here, the `packed` values is treated as local, and the type-checker then +conservatively assumes that `x'` and `y'` may also be local (since they are extracted from `packed`), and so cannot safely be returned. -Similarly, a `local` value of type `string list` means a local +Similarly, a local value of type `string list` means a local list of local strings, and none of these strings can be safely returned from a function like `f`. @@ -676,9 +675,9 @@ In particular, by defining: type 'a t = { global_ global : 'a } [@@unboxed] ``` -then a `local` value of type `string t list` is a local list of global +then a local value of type `string t list` is a local list of global strings, and while the list itself cannot be returned out of a region, the -`global` field of any of its elements can. For convenience, `base` provides +global field of any of its elements can. For convenience, `base` provides this as the type `Modes.Global.t`. The same overriding can be used on constructor arguments. To imitate the example From b48389401b85c3bdd9a240d505d2ec2f429d2211 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 12:08:40 +0000 Subject: [PATCH 07/21] move things --- jane/doc/extensions/stack/reference.md | 90 +++++++++++++------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 9a6bc892b96..2e968b8b16f 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -76,6 +76,51 @@ Global values, being allowed to escape regions, may not reference local ones, since that will make the local values escape regions, which breaks the guarantee. Local values may reference global ones. +A global value can be weakened to local, effectively "forgetting" that it +can escape regions. For instance, if there is a global `x : int list` in +scope, then this is allowed: + +```ocaml +let l = if n > 0 then stack_ (n :: x) else x in +... +``` + +Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. +However, if `n <= 0`, then `l` will be `x`, which is global. The later is +implicitly weakened to local and joins with the other branch, making the whole +expression local. + +Consider another example: + +```ocaml +let l = local_ if n > 0 then n :: x else x in +... +``` + +The `local_` annotation forces `l` to be local, which prevents `l` from +escaping the current region. As a result, the compiler might optimize `n :: x` +to be stack-allocated in the current region. However, this is not to be relied +upon - you should always use `stack_` to ensure stack allocation. + +Most OCaml types can be stack-allocated, including records, variants, +polymorphic variants, closures, boxed numbers and strings. However, certain +values cannot be stack-allocated, and will always be on the GC heap, +including: + + - Modules (including first-class modules) + + - Exceptions + (Technically, values of type `exn` can be locally allocated, but only global + ones may be raised) + + - Classes and objects + +In addition, any value that is to be put into a mutable field (for example +inside a `ref`, an `array` or a mutable record) must be global and thus cannot +be stack-allocated. Should you need to put a local value into one of these +places, you may want to check out +[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). + "Region" is a wider concept than "scope", and stack-allocated variables can outlive their scope. For example: @@ -151,51 +196,6 @@ compiler know that it is safe to still refer to `outer` from within the closure -Global values can be weakened to local, effectively "forgetting" that a -value can escape regions. For instance, if there is a global `x : int list` in -scope, then this is allowed: - -```ocaml -let l = if n > 0 then stack_ (n :: x) else x in -... -``` - -Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. -However, if `n <= 0`, then `l` will be `x`, which is global. The later is -implicitly weakened to local and joins with the other branch, making the whole -expression local. - -Consider another example: - -```ocaml -let l = local_ if n > 0 then n :: x else x in -... -``` - -The `local_` annotation forces `l` to be local, which prevents `l` from -escaping the current region. As a result, the compiler might optimize `n :: x` -to be stack-allocated in the current region. However, this is not to be relied -upon - you should always use `stack_` to ensure stack allocation. - -Most OCaml types can be stack-allocated, including records, variants, -polymorphic variants, closures, boxed numbers and strings. However, certain -values cannot be stack-allocated, and will always be on the GC heap, -including: - - - Modules (including first-class modules) - - - Exceptions - (Technically, values of type `exn` can be locally allocated, but only global - ones may be raised) - - - Classes and objects - -In addition, any value that is to be put into a mutable field (for example -inside a `ref`, an `array` or a mutable record) must be global and thus cannot -be stack-allocated. Should you need to put a local value into one of these -places, you may want to check out -[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). - ## Inference In fact, the allocations of the examples above will be on From 54d8032b6dbc49de340067c4ce6b09baf72383a4 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 12:15:09 +0000 Subject: [PATCH 08/21] polish --- jane/doc/extensions/stack/reference.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 2e968b8b16f..fa45d916bd6 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -90,17 +90,19 @@ However, if `n <= 0`, then `l` will be `x`, which is global. The later is implicitly weakened to local and joins with the other branch, making the whole expression local. -Consider another example: +You can also use the `local_` keyword to explicitly weaken a value to local. For +example: ```ocaml let l = local_ if n > 0 then n :: x else x in ... ``` -The `local_` annotation forces `l` to be local, which prevents `l` from -escaping the current region. As a result, the compiler might optimize `n :: x` -to be stack-allocated in the current region. However, this is not to be relied -upon - you should always use `stack_` to ensure stack allocation. +The `local_` keyword doesn't force stack allocation. However, it does weaken +`l` to local, which prevents `l` from escaping the current region, and as a +result the compiler might optimize `n :: x` to be stack-allocated in the current +region. However, this is not to be relied upon - you should always use `stack_` +to ensure stack allocation. Most OCaml types can be stack-allocated, including records, variants, polymorphic variants, closures, boxed numbers and strings. However, certain From f2607d3f417ac6835fc749f9760e5199d185bf2f Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 12:21:17 +0000 Subject: [PATCH 09/21] move things --- jane/doc/extensions/stack/reference.md | 46 ++++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index fa45d916bd6..0c6cbad5193 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -13,7 +13,7 @@ stack-allocates certain values is controlled or inferred from new keywords `stack_` and `local_`, whose effects are explained below. -## Stack allocation and local expressions +## Stack allocation The `stack_` keyword may be placed on an allocation to indicate that it should be stack-allocated: @@ -35,6 +35,24 @@ let stack_ abc = (42, 24) in Placing `stack_` on an expression that is not an allocation is meaningless and causes type error. +Most OCaml types can be stack-allocated, including records, variants, +polymorphic variants, closures, boxed numbers and strings. However, certain +values cannot be stack-allocated, and will always be on the GC heap, +including: + + - Modules (including first-class modules) + + - Exceptions + (Technically, values of type `exn` can be locally allocated, but only global + ones may be raised) + + - Classes and objects + +In addition, any value that is to be put into a mutable field (for example +inside a `ref`, an `array` or a mutable record) must be heap-allocated. Should you need to put a stack-allocated value into one of these +places, you may want to check out +[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). + ### Runtime behavior At runtime, stack allocations do not take place on the C stack, but on a @@ -46,7 +64,7 @@ without the need to copy returned values. The beginning of a stack frame records the stack pointer of this local stack, and the end of the stack frame resets the stack pointer to this value. -### Regions +## Regions and local values Every stack allocation takes places inside a stack frame, and is freed when the stack frame is freed. For this to be safe, stack-allocated values cannot be used @@ -76,6 +94,8 @@ Global values, being allowed to escape regions, may not reference local ones, since that will make the local values escape regions, which breaks the guarantee. Local values may reference global ones. + +### Weakening A global value can be weakened to local, effectively "forgetting" that it can escape regions. For instance, if there is a global `x : int list` in scope, then this is allowed: @@ -104,26 +124,7 @@ result the compiler might optimize `n :: x` to be stack-allocated in the current region. However, this is not to be relied upon - you should always use `stack_` to ensure stack allocation. -Most OCaml types can be stack-allocated, including records, variants, -polymorphic variants, closures, boxed numbers and strings. However, certain -values cannot be stack-allocated, and will always be on the GC heap, -including: - - - Modules (including first-class modules) - - - Exceptions - (Technically, values of type `exn` can be locally allocated, but only global - ones may be raised) - - - Classes and objects - -In addition, any value that is to be put into a mutable field (for example -inside a `ref`, an `array` or a mutable record) must be global and thus cannot -be stack-allocated. Should you need to put a local value into one of these -places, you may want to check out -[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). - - +### Region vs. Scope "Region" is a wider concept than "scope", and stack-allocated variables can outlive their scope. For example: @@ -143,6 +144,7 @@ The stack-allocated reference `r` is allocated inside the definition of type-checker ensures that it does not outlive the region that it lives in, which is the entire body of `f`. +### Other regions As well as function bodies, a region is also placed around: - Loop bodies (`while` and `for`) From 793bbe901d549f603f301f5723acde4c96495414 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 16 Jan 2025 15:40:09 +0000 Subject: [PATCH 10/21] more polish --- jane/doc/extensions/stack/intro.md | 38 ++++------- jane/doc/extensions/stack/reference.md | 95 ++++++++++---------------- 2 files changed, 50 insertions(+), 83 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 535c7f96346..954107f723c 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -5,23 +5,11 @@ See also the full feature [reference](reference.md) and [common pitfalls](pitfal Instead of allocating values normally on the GC heap, you can stack-allocate values using the new `stack_` keyword: -```ocaml -let stack_ x = { foo; bar } in -... -``` - -or equivalently, by putting the keyword on the expression itself: - ```ocaml let x = stack_ { foo; bar } in ... ``` -These values live in a region, and are available until the end of the _region_. -Region is the compile-time representation of stack frame at runtime. Usually, -each function body has a region, and stack-allocated values live in the -surrouding region. Read [the reference](reference.md) for more details. - This helps performance in a couple of ways: first, the same few hot cache lines are constantly reused, so the cache footprint is lower than usual. More importantly, stack allocations will never trigger a GC, @@ -29,11 +17,11 @@ and so they're safe to use in low-latency code that must currently be zero-alloc. However, for this to be safe, stack-allocated values must not be used after -their region ends. This is ensured by the type-checker as follows. -Stack-allocated values will be _local_ to the region they live in, and cannot -escape their region. Heap-allocated values will be _global_ and can escape any -region. If a local value tries to escape the current region, you'll see error -messages: +their stack frame is freed. This is ensured by the type-checker as follows. +Stack frames are represented as _region_ at compile time, and each +stack-allocated value lives in the surrounding region (usually a function body). +Stack-allocated values are not allowed to escape their region. If they do, +you'll see error messages: ```ocaml let foo () = @@ -58,7 +46,7 @@ allocations is meaningless and triggers type errors. Generally, OCaml functions can do whatever they like with their arguments: use them, return them, capture them in closures or store them in globals, etc. This is a problem when trying to pass around -local values, since we need to guarantee they do not +stack-allocated values, since we need to guarantee they do not escape. The remedy is that we allow the `local_` keyword to appear on @@ -70,15 +58,15 @@ let f (local_ x) = ... A local parameter is a promise by a function not to let a particular argument escape its region. In the body of f, you'll get a type error -if x escapes, but when calling f you can freely pass local values as +if x escapes, but when calling f you can freely pass stack-allocated values as the argument. This promise is visible in the type of f: ```ocaml val f : local_ 'a -> ... ``` -The function f may be equally be called with local or -global values: the `local_` annotation places obligations only on the +The function f may be equally be called with stack or +heap-allocated values: the `local_` annotation places obligations only on the definition of f, not its uses. Even if you're not interested in performance benefits, local @@ -147,7 +135,7 @@ over which values are stack-allocated, including: ... ``` - defines a function `f` which returns local values into its + defines a function `f` which returns stack-allocated values into its caller's region. - **Global fields** @@ -156,8 +144,8 @@ over which values are stack-allocated, including: type 'a t = { global_ g : 'a } ``` - defines a record type `t` whose `g` field is always known to be global - (and thus on the GC heap and may freely escape regions), even though - the record itself may be `local`. + defines a record type `t` whose `g` field is always known to be + heap-allocated (and may freely escape regions), even though the record + itself may be stack-allocated. For more details, read [the reference](./reference.md). diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 0c6cbad5193..118857f9932 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -12,7 +12,6 @@ is reclaimed, and can be immediately reused. Whether the compiler stack-allocates certain values is controlled or inferred from new keywords `stack_` and `local_`, whose effects are explained below. - ## Stack allocation The `stack_` keyword may be placed on an allocation to indicate that @@ -23,16 +22,7 @@ let abc = stack_ (42, 24) in ... ``` -Here, the tuple cell will be stack-allocated. - -Equivalently, the keyword `stack_` may precede the bound variable in a `let`: - -```ocaml -let stack_ abc = (42, 24) in -... -``` - -Placing `stack_` on an expression that is not an allocation is meaningless and +Here, the tuple cell will be stack-allocated. Placing `stack_` on an expression that is not an allocation is meaningless and causes type error. Most OCaml types can be stack-allocated, including records, variants, @@ -48,11 +38,6 @@ including: - Classes and objects -In addition, any value that is to be put into a mutable field (for example -inside a `ref`, an `array` or a mutable record) must be heap-allocated. Should you need to put a stack-allocated value into one of these -places, you may want to check out -[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). - ### Runtime behavior At runtime, stack allocations do not take place on the C stack, but on a @@ -61,40 +46,31 @@ minor heap. In particular, this allows local-returning functions (see "Use exclave_ to return a local value" below ) without the need to copy returned values. -The beginning of a stack frame records the stack pointer of this local stack, +The beginning of a stack frame records the stack pointer of this stack, and the end of the stack frame resets the stack pointer to this value. -## Regions and local values +## Regions Every stack allocation takes places inside a stack frame, and is freed when the stack frame is freed. For this to be safe, stack-allocated values cannot be used after their stack frame is freed. This runtime behavior is guaranteed at compile-time by the type checker as follows. -Every OCaml value lives in a _region_. Usually a function body has a region, -representing the function's stack frame at runtime. A stack-allocated value -lives in the region it's allocated in. We say the value is _local_ to the region -it lives in. Regions may nest, for instance when one function calls another. -Stack allocaiton always gives a value that lives in the current region, since at -runtime one can only allocate in the current stack frame. There is an outermost -region, and values living in it are _global_. Heap-allocated values live in this -region. +Usually a function body has a _region_, representing the function's stack frame at +runtime. A stack-allocated value lives in the region it's allocated in. We say +the value is _local_ to the region it lives in. A heap-allocated value is +_global_. We say that a value _escapes_ a region if it is still referenced beyond the end -of that region. The type-checker guarantees that values do not escape the region -that they live in. Since stack-allocated values live in the region representing -the stack frame containing the allocation, they are guaranteed to be never +of that region. The type-checker guarantees that local values do not escape +their region. Since stack-allocated values live in the region representing the +stack frame containing the allocation, they are guaranteed to be never referenced after their stack frame is freed. -The above guarantee means that global values can escape all regions except the -outermost one. However, we say the outermost region never ends, so values never -need to escape that region. - -Global values, being allowed to escape regions, may not reference local ones, -since that will make the local values escape regions, which breaks the +Global values can escape all regions. As a result, they may not reference local +values, since that will make the local values escape regions, which breaks the guarantee. Local values may reference global ones. - ### Weakening A global value can be weakened to local, effectively "forgetting" that it can escape regions. For instance, if there is a global `x : int list` in @@ -118,11 +94,11 @@ let l = local_ if n > 0 then n :: x else x in ... ``` -The `local_` keyword doesn't force stack allocation. However, it does weaken -`l` to local, which prevents `l` from escaping the current region, and as a -result the compiler might optimize `n :: x` to be stack-allocated in the current -region. However, this is not to be relied upon - you should always use `stack_` -to ensure stack allocation. +The `local_` keyword doesn't force stack allocation. However, it does weaken `l` +to local, which prevents `l` from escaping the current region, and as a result +the compiler _might_ optimize `n :: x` to be stack-allocated in the current +region. However, this optimization is not to be relied upon - you should always +use `stack_` to ensure stack allocation. ### Region vs. Scope "Region" is a wider concept than "scope", and stack-allocated variables can @@ -159,9 +135,14 @@ around their body, which is useful to write functions that return stack-allocated values. See "Use exclave_ to return a local value" below. ### Nested regions -Let's further explore the idea of nested regions mentioned above. Say we are in -the middle of a region, the local variables in scope are further subdivided into -two cases: +Regions may nest, for instance when one function calls another. Stack-allocated +values always lives in the surrounding region, since at runtime one can +only allocate in the current stack frame. There always exists a region at the +outermost. This region never ends and is where global (such as heap-allocated) +values live. + +To better understand the nested structure, imagine we are in a region, and the +local variables in scope are further subdivided into two cases: - **Outer-region local**: lives in an outer region, not the current region. @@ -198,8 +179,6 @@ value `outer`, while being local, was definitely not local to the region of compiler know that it is safe to still refer to `outer` from within the closure `g`? See "Closures" below for more details) - - ## Inference In fact, the allocations of the examples above will be on @@ -209,8 +188,8 @@ happens if the allocated value escapes (e.g. is stored into a global hash table) and therefore cannot be stack-allocated. With the keyword, an error will be reported, while without the keyword the allocations will occur on the GC heap as usual. Similarly, whether a value is global or local (and -hence whether certain allocation can be on stack) is inferred by the -type-checker, although the `local_` keyword may be used to specify it. +hence whether certain allocation can be optimized to be on stack) is inferred by +the type-checker, although the `local_` keyword may be used to specify it. Inference does not cross file boundaries. If local annotations subject to inference appear in the type of a module (e.g. since they can appear in @@ -248,8 +227,6 @@ let f1 ~foo:_ = () let f2 x = f1 ~foo:(Some x) (* [Some x] is heap allocated *) ``` - - ## Function types and local arguments Function types now accept the `local_` keyword in both argument and return @@ -260,13 +237,13 @@ positions, leading to four distinct types of function: a -> local_ b local_ a -> local_ b -In all cases, the `local_` annotation means "local to the caller's region" -, or equivalently "outer-region local to the callee's region" if the callee has -a region. +In all cases, the `local_` annotation means "local to the callsite's surrounding +region" , or equivalently "outer-region local to the function's region" if the +function has a region. In argument positions, `local_` indicates that the function may be passed local values. As always, the `local_` keyword does not *require* -a stack-allocated value, and you may pass global values to such functions. In +a local value, and you may pass global values to such functions. In effect, a function of type `local_ a -> b` is a function accepting `a` and returning `b` that promises not to capture any reference to its argument. @@ -283,7 +260,7 @@ let f (local_ x) = ... ``` Inside the definition of `f`, the argument `x` is outer-region local: that is, -while it may be locally allocated, it is known not to have been allocated during +while it may be stack-allocated, it is known not to have been allocated during `f` itself, and thus may safely be returned from `f`. For example: ```ocaml @@ -305,8 +282,8 @@ region. In contrast, `f3` is an error. The value `42 :: x` refers to a local value `x`, which means it cannot be global. Therefore, it must be stack-allocated, and it - is allocated within region of `f3`. When this region ends, the any-region local -value `42 :: x` is not allowed to escape it. + is allocated within the region of `f3`. When this region ends, the any-region +local value `42 :: x` is not allowed to escape it. It is possible to write functions like `f3` that return stack-allocated values, but this requires explicit annotation, as it would otherwise be easy to @@ -699,13 +676,15 @@ let f () = Mutable fields are always `global_`, including array elements. That is, while you may create local `ref`s or arrays, their contents must always be global. +Should you need to put a local value into one of these +places, you may want to check out +[`ppx_globalize`](https://github.com/janestreet/ppx_globalize). This restriction may be lifted somewhat in future: the tricky part is that naively permitting mutability might allow an older local mutable value to be mutated to point to a younger one, creating a dangling reference to an escaping value when the younger one's region ends. - ## Curried functions The function type constructor in OCaml is right-associative, so that these are From 3349cf0824f9fe9a8a444962317cb03ccfc4617d Mon Sep 17 00:00:00 2001 From: Richard Eisenberg Date: Mon, 20 Jan 2025 15:42:12 -0500 Subject: [PATCH 11/21] Some edits from Richard --- jane/doc/extensions/modes/reference.md | 2 +- jane/doc/extensions/stack/intro.md | 37 +++-- jane/doc/extensions/stack/reference.md | 190 ++++++++++++---------- jane/doc/extensions/uniqueness/intro.md | 2 +- jane/doc/proposals/unboxed-types/kinds.md | 6 +- 5 files changed, 134 insertions(+), 103 deletions(-) diff --git a/jane/doc/extensions/modes/reference.md b/jane/doc/extensions/modes/reference.md index 0df097c5056..9abf44e335f 100644 --- a/jane/doc/extensions/modes/reference.md +++ b/jane/doc/extensions/modes/reference.md @@ -5,7 +5,7 @@ OCaml. The mode system in the compiler tracks various properties of values, so that certain performance-enhancing operations can be performed safely. For example: -- Locality tracks escaping. See [the local allocations reference](../local/reference.md) +- Locality tracks escaping. See [the local allocations reference](../stack/reference.md) - Uniqueness and linearity tracks aliasing. See [the uniqueness reference](../uniqueness/reference.md) - Portability and contention tracks inter-thread sharing. diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 954107f723c..4ac652f4c29 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -2,20 +2,32 @@ See also the full feature [reference](reference.md) and [common pitfalls](pitfalls.md). -Instead of allocating values normally on the GC heap, you can stack-allocate -values using the new `stack_` keyword: - -```ocaml -let x = stack_ { foo; bar } in -... -``` - +This page describes how OCaml sometimes allocates values on a stack, +as opposed to its usual behavior of allocating on the heap. This helps performance in a couple of ways: first, the same few hot cache lines are constantly reused, so the cache footprint is lower than usual. More importantly, stack allocations will never trigger a GC, and so they're safe to use in low-latency code that must currently be zero-alloc. +Because of these advantages, values are allocated on a stack whenever +possible. Of course, not all values can be allocated on a stack: a value that is +used beyond the scope of its introduction must be on the heap. Accordingly, +the compiler uses the _locality_ of a value to determine where it will be +allocated: _local_ values go on the stack, while _global_ ones must go on the +heap. + +Though type inference will infer where stack allocation is possible, it is +often wise to annotate places where you wish to enforce locality and stack +allocation. This can be done in two ways, by either giving variables or +values the `local` mode or by labeling an allocation as a `stack_` allocation: + +```ocaml +let local_ x1 = { foo; bar } in +let x2 = stack_ { foo; bar } in +... +``` + However, for this to be safe, stack-allocated values must not be used after their stack frame is freed. This is ensured by the type-checker as follows. Stack frames are represented as _region_ at compile time, and each @@ -37,9 +49,10 @@ although this requires code changes to use the new `caml_alloc_local` instead of `caml_alloc`. A few types of allocation cannot be stack-allocated, though, including first-class modules, classes and objects, and exceptions. The contents of mutable fields (inside `ref`s, `array`s and mutable record fields) also -cannot be stack-allocated. Annotating `stack_` on expressions that are not -allocations is meaningless and triggers type errors. +cannot be stack-allocated. +The `stack_` keyword must immediately precede an allocation; putting it +anywhere else (like on a function call) leads to a type error. ## Local parameters @@ -65,7 +78,7 @@ the argument. This promise is visible in the type of f: val f : local_ 'a -> ... ``` -The function f may be equally be called with stack or +The function f may be equally be called with stack- or heap-allocated values: the `local_` annotation places obligations only on the definition of f, not its uses. @@ -135,7 +148,7 @@ over which values are stack-allocated, including: ... ``` - defines a function `f` which returns stack-allocated values into its + defines a function `f` which puts its stack-allocated values in its caller's region. - **Global fields** diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 118857f9932..c611826218f 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -22,8 +22,8 @@ let abc = stack_ (42, 24) in ... ``` -Here, the tuple cell will be stack-allocated. Placing `stack_` on an expression that is not an allocation is meaningless and -causes type error. +Here, the tuple cell will be stack-allocated. Placing `stack_` on an expression +that is not an allocation is meaningless and causes type error. Most OCaml types can be stack-allocated, including records, variants, polymorphic variants, closures, boxed numbers and strings. However, certain @@ -36,28 +36,29 @@ including: (Technically, values of type `exn` can be locally allocated, but only global ones may be raised) - - Classes and objects + - Objects ### Runtime behavior -At runtime, stack allocations do not take place on the C stack, but on a +At runtime, stack allocations do not take place on the function call stack, but on a separately-allocated stack that follows the same layout as the OCaml minor heap. In particular, this allows local-returning functions -(see "Use exclave_ to return a local value" below ) +(see "Use `exclave_` to return a local value" below) without the need to copy returned values. -The beginning of a stack frame records the stack pointer of this stack, -and the end of the stack frame resets the stack pointer to this value. +The runtime records the stack pointer when entering a new stack frame, +and leaving that stack frame resets the stack pointer to that value. ## Regions -Every stack allocation takes places inside a stack frame, and is freed when the +Every stack allocation takes places inside a stack frame and is freed when the stack frame is freed. For this to be safe, stack-allocated values cannot be used -after their stack frame is freed. This runtime behavior is guaranteed at +after their stack frame is freed. This property is guaranteed at compile-time by the type checker as follows. -Usually a function body has a _region_, representing the function's stack frame at -runtime. A stack-allocated value lives in the region it's allocated in. We say +A function body defines a _region_: a contiguous stretch of code, all of whose +stack allocations go into the same stack frame. +A stack-allocated value lives in the region it's allocated in. We say the value is _local_ to the region it lives in. A heap-allocated value is _global_. @@ -72,6 +73,7 @@ values, since that will make the local values escape regions, which breaks the guarantee. Local values may reference global ones. ### Weakening + A global value can be weakened to local, effectively "forgetting" that it can escape regions. For instance, if there is a global `x : int list` in scope, then this is allowed: @@ -82,8 +84,9 @@ let l = if n > 0 then stack_ (n :: x) else x in ``` Here, if `n > 0`, then `l` will be a stack-allocated cons cell and thus local. -However, if `n <= 0`, then `l` will be `x`, which is global. The later is -implicitly weakened to local and joins with the other branch, making the whole +However, if `n <= 0`, then `l` will be `x`, which is global. The latter is +implicitly weakened to local (because both branches of an `if` must have the +same locality), making the whole expression local. You can also use the `local_` keyword to explicitly weaken a value to local. For @@ -96,18 +99,20 @@ let l = local_ if n > 0 then n :: x else x in The `local_` keyword doesn't force stack allocation. However, it does weaken `l` to local, which prevents `l` from escaping the current region, and as a result -the compiler _might_ optimize `n :: x` to be stack-allocated in the current -region. However, this optimization is not to be relied upon - you should always -use `stack_` to ensure stack allocation. +the compiler will optimize `n :: x` to be stack-allocated in the current +region. However, users may wish to use `stack_` to ensure stack allocation, +as refactoring code can make an allocation that was previously on the stack +silenetly move to the heap. ### Region vs. Scope -"Region" is a wider concept than "scope", and stack-allocated variables can + +*Region* is a wider concept than *scope*, and stack-allocated variables can outlive their scope. For example: ```ocaml let f () = - let stack_ counter = - let stack_ r = ref 42 in + let counter = + let r = stack_ (ref 42) in incr r; r in @@ -121,51 +126,47 @@ type-checker ensures that it does not outlive the region that it lives in, which is the entire body of `f`. ### Other regions + As well as function bodies, a region is also placed around: - Loop bodies (`while` and `for`) - Lazy expressions (`lazy ...`) - - Module bindings (`let x = ...` at module level, including in sub-modules) + - Module bindings (`let x = ...;;` at module level, including in sub-modules) Module bindings are wrapped in regions to enforce the rule (as mentioned above) that modules never contain local values. -Additionally, it is possible to write functions that do *not* have a region -around their body, which is useful to write functions that return -stack-allocated values. See "Use exclave_ to return a local value" below. +Additionally, it is possible to write functions whose region ends before the +function does, which is useful to write functions that return +stack-allocated values. See "Use `exclave_` to return a local value" below. ### Nested regions -Regions may nest, for instance when one function calls another. Stack-allocated -values always lives in the surrounding region, since at runtime one can -only allocate in the current stack frame. There always exists a region at the -outermost. This region never ends and is where global (such as heap-allocated) -values live. -To better understand the nested structure, imagine we are in a region, and the -local variables in scope are further subdivided into two cases: - - - **Outer-region local**: lives in an outer region, not the current region. +Regions nest, for instance when defining a local function. Stack-allocated +values always live in the inner-most region, since at runtime one can +allocate only in the current stack frame. There is an outermost region, +including an entire file. This region never ends and is where global (heap-allocated) +values live. - - **Any-region local**: lives in an unknown region, potentially the current - region. +One subtlety is that we wish to treat `local` variables from the current region +differently than `local` variables from an enclosing region. Variables from an +enclosing region are called *regional*. For instance: ```ocaml let f () = - let stack_ outer = ref 42 in + let local_ outer = ref 42 in let g () = - let stack_ inner = ref 42 in + let local_ inner = ref 42 in ?? in ... ``` At the point marked `??` inside `g`, both `outer` and `inner` are -stack-allocated values and thus local. However, only `inner` is any-region -local, having been allocated in `g`'s region. The value `outer` is instead -outer-region local: it is stack-allocated but from a region outer than `g`'s -own. +local values; they will both be allocated on the stack at runtime. However, +`inner` is local to `g`, while (within `g`) `outer` is regional. So, if we replace `??` with `inner`, we see an error: @@ -179,10 +180,25 @@ value `outer`, while being local, was definitely not local to the region of compiler know that it is safe to still refer to `outer` from within the closure `g`? See "Closures" below for more details) +This situation also arises with local parameters. For example: + +```ocaml +let f (local_ x) = + let local_ y = 3 :: x in + ?? +``` + +Both `x` and `y` are local and cannot, in general, escape a region. However, +filling `??` in with `x` (but not `y`) is allowed. This is because we know that +the value pointed to by `x` must have existed before `f` started and thus is +guaranteed to be allocated in a pre-existing stack frame (or on the heap). +In contrast, `y` will point to a cons cell allocated in the current stack frame; +returning `y` from `f` would be unsafe (and is a type error). + ## Inference In fact, the allocations of the examples above will be on -stack even without the `stack_` keyword, if it is safe to do +stack even without the `stack_` or `local_` keywords, if it is safe to do so. The presence of the keyword on an allocation only affects what happens if the allocated value escapes (e.g. is stored into a global hash table) and therefore cannot be stack-allocated. With the keyword, an error @@ -194,7 +210,7 @@ the type-checker, although the `local_` keyword may be used to specify it. Inference does not cross file boundaries. If local annotations subject to inference appear in the type of a module (e.g. since they can appear in function types, see below) then inference will resolve them according to what -appears in the `.mli`. If there is no `.mli` file, then inference will always +appears in the mli. If there is no mli file, then inference will always choose global for anything that can be accessed from another file. Local annotations (or the lack thereof) in the mli don't affect inference @@ -215,7 +231,7 @@ let f2 x = f1 ~foo:(Some x) (* [Some x] is stack allocated *) in the flambda-backend Git repo. The ensuing paragraph is related to that note; we can remove this comment when the note is resolved. --> -However, a missing mli *does* affect inference within the ml. As a conservative +However, a missing mli *does* affect inference within the ml file. As a conservative rule of thumb, function arguments in an mli-less file will default to global unless the function parameter or argument is annotated with `local_`. This is due to an implementation detail of the type-checker and is not fundamental, but @@ -237,13 +253,13 @@ positions, leading to four distinct types of function: a -> local_ b local_ a -> local_ b -In all cases, the `local_` annotation means "local to the callsite's surrounding -region" , or equivalently "outer-region local to the function's region" if the -function has a region. +In all cases, the `local_` annotation means "local to the call site's surrounding +region" , or equivalently "regional to the function's region". In argument positions, `local_` indicates that the function may be passed local values. As always, the `local_` keyword does not *require* -a local value, and you may pass global values to such functions. In +a local value, and you may pass global values to such functions. (This is an +example of the fact that global values can always be weakened to local ones.) In effect, a function of type `local_ a -> b` is a function accepting `a` and returning `b` that promises not to capture any reference to its argument. @@ -259,7 +275,7 @@ A function with a local argument can be defined by annotating the argument as let f (local_ x) = ... ``` -Inside the definition of `f`, the argument `x` is outer-region local: that is, +As we saw above, inside the definition of `f`, the argument `x` is regional: that is, while it may be stack-allocated, it is known not to have been allocated during `f` itself, and thus may safely be returned from `f`. For example: @@ -286,12 +302,12 @@ which means it cannot be global. Therefore, it must be stack-allocated, and it local value `42 :: x` is not allowed to escape it. It is possible to write functions like `f3` that return stack-allocated -values, but this requires explicit annotation, as it would otherwise be easy to -do by mistake. See "Use exclave_ to return a local value" below. +values, but this requires an explicit annotation, as it would otherwise be easy to +do by mistake. See "Use `exclave_` to return a local value" below. Like local variables, inference can determine whether function arguments are local. However, note that for arguments of exported functions to be local, the -`local_` keyword must appear in their declarations in the corresponding `.mli` +`local_` keyword must appear in their declarations in the corresponding mli file. @@ -316,10 +332,10 @@ let f () = Here, since `g` refers to the local value `outer`, the closure `g` must itself be stack-allocated. (As always, this is deduced by inference, and an explicit -`stack_` annotation on `g` is not needed). +`stack_` or `local_` annotation on `g` is not needed.) -This then means that `g` is not allowed to escape its region, i.e. the body of -`f`. `f` may call `g` but may not return the closure. This guarantees that `g` +This then means that `g` is not allowed to escape its region (the body of +`f`). `f` may call `g` but may not return the closure. This guarantees that `g` will only run before `f` has ended, which is what makes it safe to refer to `outer` from within `g`. @@ -329,7 +345,7 @@ following function for computing the length of a list: ```ocaml let length xs = - let stack_ count = ref 0 in + let count = stack_ (ref 0) in List.iter xs ~f:(fun () -> incr count); !count ``` @@ -392,19 +408,19 @@ Therefore, when a function ends in a tail call, that function's region ends: - but before control is transferred to the callee. This early ending of the region introduces some restrictions, as values used in -tail calls then count as escaping the region. In particular, any-region local +tail calls then count as escaping the region. In particular, local (but not *regional*) values may not be passed to tail calls: ```ocaml let f1 () = - let stack_ r = ref 42 in + let r = stack_ (ref 42) in some_func r ^ Error: This local value escapes its region Hint: This argument cannot be local, because this is a tail call ``` -and any-region local closures may not be tail-called: +and local closures may not be tail-called: ```ocaml let f2 () = @@ -420,7 +436,7 @@ resolved by moving the call so that it is not syntactically a tail call: ```ocaml let f1 () = - let stack_ r = ref 42 in + let r = stack_ (ref 42) in let res = some_func r in res @@ -430,12 +446,12 @@ let f2 () = res ``` -or by annotating the call with the `[@nontail]` attribute, that +or by annotating the call with the `[@nontail]` attribute, which prevents it from being a tail call: ```ocaml let f1 () = - let stack_ r = ref 42 in + let r = stack_ (ref 42) in some_func r [@nontail] let f2 () = @@ -443,10 +459,10 @@ let f2 () = g () [@nontail] ``` -These changes makes the local values (`r` and `g`) stay available until after +These changes make the local values (`r` and `g`) stay available until after the call has returned. -Note that values which are outer-region local rather than any-region local (that +Note that values which are regional rather than purely local (that is, local values that were passed into this region from outside) may be used in tail calls, as the early closing of the region does not affect them: @@ -475,7 +491,7 @@ let f () = ``` Here, inference will detect that `counter` does not escape and will stack-allocate -the reference. However, this changes if we try to abstract out +the reference (assuming that `incr` takes its argument `local_`ly). However, this changes if we try to abstract out `counter` to its own module: ```ocaml @@ -519,13 +535,14 @@ allows its stack-allocation. The allocation will only be cleaned up when the region of `f` ends. ## Delaying exclaves + In the previous section, the example function exits its own region immediately, which allows allocating and returning in the caller's region. This approach, however, has certain disadvantages. Consider the following example: ```ocaml let f (local_ x) = exclave_ - let stack_ y = (complex computation on x) in + let local_ y = (complex computation on x) in if y then None else (Some x) ``` @@ -537,14 +554,15 @@ upon the function's return, we delay `exclave_` as follows: ```ocaml let f (local_ x) = - let stack_ y = (complex computation on x) in + let local_ y = (complex computation on x) in if y then exclave_ None else exclave_ Some x ``` In this example, the function `f` has a region where the allocation for the complex computation occurs. This region is terminated by `exclave_`, releasing -all temporary allocations. Both `None` and `Some x` are outer-region local and are allowed to be returned. In summary, the +all temporary allocations. Both `None` and `Some x` are allocated in the +caller's stack frame and are allowed to be returned. In summary, the temporary allocations in the `f`'s region are promptly released, and the result allocated in the caller's region is returned. @@ -639,7 +657,7 @@ This can be overridden for record types, by annotating some fields with type ('a, 'b) t = { global_ foo : 'a; bar: 'b } let f () = - let stack_ packed = {foo=x; bar=y} in + let packed = stack_ {foo=x; bar=y} in let {foo; bar} = packed in foo ``` @@ -667,7 +685,7 @@ for record fields: type ('a, 'b) t = Foo of global_ 'a * 'b let f () = - let stack_ packed = Foo (x, y) in + let packed = stack_ (Foo (x, y)) in match packed with | Foo (foo, bar) -> foo ``` @@ -751,13 +769,14 @@ of currying, a stack-allocated function has a different type than a heap-allocated one. ### Currying of local closures + Suppose we are inside the definition of a function, and there is in scope a local value `counter` of type `int ref`. Then of the following two seemingly-identical definitions, the first is accepted and the second is rejected: ```ocaml -let stack_ f : int -> int -> int = fun a b -> a + b + !counter in +let local_ f : int -> int -> int = fun a b -> a + b + !counter in ... let f : int -> int -> int = stack_ fun a b -> a + b + !counter in @@ -836,17 +855,11 @@ making all of `a`,`b` and `c` local if any of `x`, `y` and `z` are. ## Primitive definitions -Sometimes we might want a function that can be instantiated with different modes. -For example, we might want a single `id` function that can work as either of the -following: - -```ocaml -id : local_ 'a -> local_ a -id : 'a -> 'a -``` -Mode polymorphism is not available yet in general, but a different option is -available for `%`-primitives. +A limited form of mode polymorphism is available for primivites, defined +with `external`. The implementation of primitives defined within the compiler +(with names starting with `%`) can even branch on whether the primitive +is used in a non-escaping context. In the interface for the stdlib (and as re-exported by Base), this feature is enabled by use of the `[@local_opt]` annotation on `external` declarations. For @@ -855,22 +868,27 @@ example, we have the following: ```ocaml external id : ('a[@local_opt]) -> ('a[@local_opt]) = "%identity" ``` -which achieves what we wanted above. + +This declaration means that `id` can have either of the following types: + +```ocaml +id : local_ 'a -> local_ 'a +id : 'a -> 'a +``` Notice that the two `[@local_opt]`s act in unison: either both `local_`s are -present or neither is. This allows for a limited form of mode-polymorphism for -`external`s (only). Nothing checks that the locality ascriptions are sound, +present or neither is. Nothing checks that the locality ascriptions are sound, though, so use this feature with much caution. In the case of `id`, all is well, but if the two `[@local_opt]`s did not act in unison (that is, they varied independently), it would not be: `id : local_ 'a -> 'a` allows a local value to escape. Moreover, since primitives are guaranteed to be inlined at every use site, -it can have different runtime behavior (such as allocation) according to the instantiated modes. For example, `ref` is defined as +they can have different runtime behavior (such as allocation) according to the instantiated modes. For example, `ref` is defined as ```ocaml external ref : 'a -> ('a ref[@local_opt]) = "%makemutable" ``` -which would allocate the cell on GC heap or on stack, depending on the -instantiated mode. +which allocates the cell on the GC heap or on the stack, depending on the +inferred mode. diff --git a/jane/doc/extensions/uniqueness/intro.md b/jane/doc/extensions/uniqueness/intro.md index 1ca9f3092a9..2957d1872a2 100644 --- a/jane/doc/extensions/uniqueness/intro.md +++ b/jane/doc/extensions/uniqueness/intro.md @@ -35,7 +35,7 @@ let delay_free : t @ unique -> (unit -> unit) @ once = fun t -> fun () -> free t These modes form two mode axes: the _uniqueness_ of a value is either `unique` or `aliased`, while the _affinity_ of a value is `once` or `many`. Similar to -[locality](../local/intro.md), uniqueness and affinity are deep properties. If a +[locality](../stack/intro.md), uniqueness and affinity are deep properties. If a value is at mode `unique` then all of its children are also `unique`. If a value is `once` then all of the closures it contains are also at mode `once`. diff --git a/jane/doc/proposals/unboxed-types/kinds.md b/jane/doc/proposals/unboxed-types/kinds.md index 85dac6bb13e..184251c086a 100644 --- a/jane/doc/proposals/unboxed-types/kinds.md +++ b/jane/doc/proposals/unboxed-types/kinds.md @@ -8,7 +8,7 @@ necessary to read all of the details from the other page. ## Motivation: mode crossing -In a language without modes (such as [`local`](../../extensions/local/intro.md) +In a language without modes (such as [`local`](../../extensions/stack/intro.md) or [`sync`](../modes/data-race-freedom.md)), classifying a type by its [layout](index.md) would be enough. However, our experience with local types suggest that users will enjoy the ability to control whether types can mode-cross, and kinds are @@ -160,7 +160,7 @@ and a kind of the same name. ## The externality mode axis The [locality mode axis is well -described](../../extensions/local/intro.md). However, this page newly introduces +described](../../extensions/stack/intro.md). However, this page newly introduces the *externality* axis. It is arranged like this: ``` @@ -389,4 +389,4 @@ Of the initial layouts we've imagined: Use ; or ,? It's kind-of a tuple, but it's unordered. And it uses `with` syntax. It really should be ;. -Use braces? Nah. \ No newline at end of file +Use braces? Nah. From 3b0bc6af9215deb17a3ae47aaec11d59887ff9e2 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Wed, 12 Feb 2025 16:07:14 +0000 Subject: [PATCH 12/21] clean up unsupported syntax --- jane/doc/extensions/stack/intro.md | 2 +- jane/doc/extensions/stack/reference.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 4ac652f4c29..17317fc20a2 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -136,7 +136,7 @@ over which values are stack-allocated, including: - **Stack-allocated closures** ```ocaml - let stack_ f a b c = ... + let f = stack_ (fun a b c -> ...) ``` defines a function `f` whose closure is itself stack-allocated. diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index c611826218f..d1ee102ab3f 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -322,9 +322,9 @@ Consider again the example from "Variables and regions" above: ```ocaml let f () = - let stack_ outer = ref 42 in + let outer = stack_ (ref 42) in let g () = - let stack_ inner = ref 42 in + let inner = stack_ (ref 42) in outer in ... @@ -424,7 +424,7 @@ and local closures may not be tail-called: ```ocaml let f2 () = - let stack_ g () = 42 in + let g = stack_ (fun () -> 42) in g () ^ Error: This local value escapes its region @@ -441,7 +441,7 @@ let f1 () = res let f2 () = - let stack_ g () = 42 in + let g = stack_ (fun () -> 42) in let res = g () in res ``` @@ -455,7 +455,7 @@ let f1 () = some_func r [@nontail] let f2 () = - let stack_ g () = 42 in + let g = stack_ (fun () -> 42) in g () [@nontail] ``` @@ -637,7 +637,7 @@ both global: ```ocaml let f () = - let stack_ packed = (x, y) in + let packed = stack_ (x, y) in let x', y' = packed in x' ``` From 73b2806c9514dbda7422b58cf3ec6c216150e0d3 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Wed, 12 Feb 2025 16:25:09 +0000 Subject: [PATCH 13/21] more improve --- jane/doc/extensions/stack/intro.md | 6 ++++-- jane/doc/extensions/stack/reference.md | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 17317fc20a2..2adaf9ff8a1 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -51,8 +51,10 @@ including first-class modules, classes and objects, and exceptions. The contents of mutable fields (inside `ref`s, `array`s and mutable record fields) also cannot be stack-allocated. -The `stack_` keyword must immediately precede an allocation; putting it -anywhere else (like on a function call) leads to a type error. +The `stack_` keyword works shallowly: it only forces the immediately succeeding allocation + to be on stack. Putting it before an expression that is not an allocation (such as a + complete function application) leads to a type error. Stack allocating closures resulted + from partial applications will be supported in the future. ## Local parameters diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index d1ee102ab3f..9f4261e94bf 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -22,8 +22,22 @@ let abc = stack_ (42, 24) in ... ``` -Here, the tuple cell will be stack-allocated. Placing `stack_` on an expression -that is not an allocation is meaningless and causes type error. +Here, the tuple cell will be stack-allocated. The `stack_` keyword works shallowly: it +only forces the immediately succeeding allocation to be on stack. In the following +example, the outer tuple is guaranteed to be on stack, while the inner one is not ( +although likely to be due to optimization). +```ocaml +let abc = stack_ (42, (24, 42)) in +... +``` + +Placing `stack_` on an expression that is not an allocation is meaningless and causes type +error: +```ocaml +let f = ref (stack_ `Foo) + ^^^^ +Error: This expression is not an allocation site. +``` Most OCaml types can be stack-allocated, including records, variants, polymorphic variants, closures, boxed numbers and strings. However, certain From e153bfcbbed9dfdc98f45c907a80b5008ad075af Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Wed, 12 Feb 2025 16:39:42 +0000 Subject: [PATCH 14/21] more improve --- jane/doc/extensions/stack/reference.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 9f4261e94bf..b7aa81754ef 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -202,12 +202,11 @@ let f (local_ x) = ?? ``` -Both `x` and `y` are local and cannot, in general, escape a region. However, -filling `??` in with `x` (but not `y`) is allowed. This is because we know that -the value pointed to by `x` must have existed before `f` started and thus is -guaranteed to be allocated in a pre-existing stack frame (or on the heap). -In contrast, `y` will point to a cons cell allocated in the current stack frame; -returning `y` from `f` would be unsafe (and is a type error). +Both `x` and `y` are local and cannot, in general, escape a region. However, filling `??` +in with `x` (but not `y`) is allowed. This is because we know that `x` lives outside of +`f`'s region and thus is guaranteed to be allocated in a pre-existing stack frame (or on +the heap) which will continue to exist after `f` ends. In contrast, `y` is a cons cell +allocated in the current stack frame, which will be destroyed after `f` ends. ## Inference From d7cee044e54c9ca4eafe92941c6e914ffdc7205a Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Thu, 20 Feb 2025 11:24:55 +0000 Subject: [PATCH 15/21] improve primitive section --- jane/doc/extensions/stack/reference.md | 30 ++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index b7aa81754ef..5de14d6f11d 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -867,12 +867,11 @@ several variables, the type-checker treats it as such rather than making all of `a`,`b` and `c` local if any of `x`, `y` and `z` are. -## Primitive definitions +## Primitive declarations +### Mode polymorphism A limited form of mode polymorphism is available for primivites, defined -with `external`. The implementation of primitives defined within the compiler -(with names starting with `%`) can even branch on whether the primitive -is used in a non-escaping context. +with `external`. In the interface for the stdlib (and as re-exported by Base), this feature is enabled by use of the `[@local_opt]` annotation on `external` declarations. For @@ -896,8 +895,27 @@ but if the two `[@local_opt]`s did not act in unison (that is, they varied independently), it would not be: `id : local_ 'a -> 'a` allows a local value to escape. -Moreover, since primitives are guaranteed to be inlined at every use site, -they can have different runtime behavior (such as allocation) according to the instantiated modes. For example, `ref` is defined as +### Stack allocation + +Primitives defined within the compiler (with names starting with `%`) are +inlined at every use site, and can have different runtime behavior (such as +allocation) at each use site. For example, primitives that return allocated +values will allocate the value on stack if declared to be local-returning: + +```ocaml +external ref_stack : 'a -> local_ 'a ref = "%makemutable" +external ref_heap : 'a -> 'a ref = "%makemutable" + +let r_stack = ref_stack "hello" in +let r_heap = ref_heap "hello" in +let r_error = stack_ (ref_heap "hello") in +... +``` + +In this example, `r_stack` will always be on stack even without `stack_` +annotation; `r_heap` will always be on heap; and `r_error` will trigger type +error. We can further use the `[@local_opt]` attribute to declare an allocation +polymorphic `ref`: ```ocaml external ref : 'a -> ('a ref[@local_opt]) = "%makemutable" From 2349a1bfa8e44c3b067f42d888890dd01ff85f28 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 11:27:56 +0000 Subject: [PATCH 16/21] say "outer-local" --- jane/doc/extensions/stack/reference.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 5de14d6f11d..b517b79e480 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -163,8 +163,7 @@ including an entire file. This region never ends and is where global (heap-alloc values live. One subtlety is that we wish to treat `local` variables from the current region -differently than `local` variables from an enclosing region. Variables from an -enclosing region are called *regional*. +differently than `local` variables from an enclosing region. The latter is called *outer-local*. For instance: @@ -180,7 +179,7 @@ let f () = At the point marked `??` inside `g`, both `outer` and `inner` are local values; they will both be allocated on the stack at runtime. However, -`inner` is local to `g`, while (within `g`) `outer` is regional. +`inner` is local to `g`, while (within `g`) `outer` is outer-local. So, if we replace `??` with `inner`, we see an error: @@ -267,7 +266,7 @@ positions, leading to four distinct types of function: local_ a -> local_ b In all cases, the `local_` annotation means "local to the call site's surrounding -region" , or equivalently "regional to the function's region". +region" , or equivalently "outer-local to the function's region". In argument positions, `local_` indicates that the function may be passed local values. As always, the `local_` keyword does not *require* @@ -288,7 +287,7 @@ A function with a local argument can be defined by annotating the argument as let f (local_ x) = ... ``` -As we saw above, inside the definition of `f`, the argument `x` is regional: that is, +As we saw above, inside the definition of `f`, the argument `x` is outer-local: that is, while it may be stack-allocated, it is known not to have been allocated during `f` itself, and thus may safely be returned from `f`. For example: @@ -421,7 +420,7 @@ Therefore, when a function ends in a tail call, that function's region ends: - but before control is transferred to the callee. This early ending of the region introduces some restrictions, as values used in -tail calls then count as escaping the region. In particular, local (but not *regional*) +tail calls then count as escaping the region. In particular, local values may not be passed to tail calls: ```ocaml @@ -475,8 +474,7 @@ let f2 () = These changes make the local values (`r` and `g`) stay available until after the call has returned. -Note that values which are regional rather than purely local (that -is, local values that were passed into this region from outside) may be used in +Note that values which are outer-local (see "Nested regions") may be used in tail calls, as the early closing of the region does not affect them: ```ocaml From 988c62e52c05f68bfb36ab663bd7a77e177253d8 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 11:38:49 +0000 Subject: [PATCH 17/21] minor fixes --- jane/doc/extensions/stack/reference.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index b517b79e480..17ab3c4cb75 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -50,7 +50,7 @@ including: (Technically, values of type `exn` can be locally allocated, but only global ones may be raised) - - Objects + - Classes and Objects ### Runtime behavior @@ -177,9 +177,9 @@ let f () = ... ``` -At the point marked `??` inside `g`, both `outer` and `inner` are -local values; they will both be allocated on the stack at runtime. However, -`inner` is local to `g`, while (within `g`) `outer` is outer-local. +At the point marked `??` inside `g`, both `outer` and `inner` are local values, +but they live in different regions: `inner` lives in `g`'s region, while `outer` +lives in the outer region and thus is outer-local. So, if we replace `??` with `inner`, we see an error: From 3e8a12c61744de35969b194053058dc76350ef17 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 11:40:27 +0000 Subject: [PATCH 18/21] minor fix --- jane/doc/extensions/stack/intro.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 2adaf9ff8a1..5564989930c 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -19,11 +19,9 @@ heap. Though type inference will infer where stack allocation is possible, it is often wise to annotate places where you wish to enforce locality and stack -allocation. This can be done in two ways, by either giving variables or -values the `local` mode or by labeling an allocation as a `stack_` allocation: +allocation. This can be done by labeling an allocation as a `stack_` allocation: ```ocaml -let local_ x1 = { foo; bar } in let x2 = stack_ { foo; bar } in ... ``` From 45742aa364f3fec30fc711eeeb84b7d1b390986f Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 11:47:01 +0000 Subject: [PATCH 19/21] minor fixes --- jane/doc/extensions/stack/intro.md | 9 +++++---- jane/doc/extensions/stack/reference.md | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/jane/doc/extensions/stack/intro.md b/jane/doc/extensions/stack/intro.md index 5564989930c..cfab5c04d84 100644 --- a/jane/doc/extensions/stack/intro.md +++ b/jane/doc/extensions/stack/intro.md @@ -28,7 +28,7 @@ let x2 = stack_ { foo; bar } in However, for this to be safe, stack-allocated values must not be used after their stack frame is freed. This is ensured by the type-checker as follows. -Stack frames are represented as _region_ at compile time, and each +A stack frames is represented as a _region_ at compile time, and each stack-allocated value lives in the surrounding region (usually a function body). Stack-allocated values are not allowed to escape their region. If they do, you'll see error messages: @@ -49,10 +49,9 @@ including first-class modules, classes and objects, and exceptions. The contents of mutable fields (inside `ref`s, `array`s and mutable record fields) also cannot be stack-allocated. -The `stack_` keyword works shallowly: it only forces the immediately succeeding allocation +The `stack_` keyword works shallowly: it only forces the immediately following allocation to be on stack. Putting it before an expression that is not an allocation (such as a - complete function application) leads to a type error. Stack allocating closures resulted - from partial applications will be supported in the future. + complete function application) leads to a type error. ## Local parameters @@ -82,6 +81,8 @@ The function f may be equally be called with stack- or heap-allocated values: the `local_` annotation places obligations only on the definition of f, not its uses. + + Even if you're not interested in performance benefits, local parameters are a useful new tool for structuring APIs. For instance, consider a function that accepts a callback, to which it passes some diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 17ab3c4cb75..a0dc4c94194 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -23,7 +23,7 @@ let abc = stack_ (42, 24) in ``` Here, the tuple cell will be stack-allocated. The `stack_` keyword works shallowly: it -only forces the immediately succeeding allocation to be on stack. In the following +only forces the immediately following allocation to be on stack. In the following example, the outer tuple is guaranteed to be on stack, while the inner one is not ( although likely to be due to optimization). ```ocaml @@ -31,8 +31,8 @@ let abc = stack_ (42, (24, 42)) in ... ``` -Placing `stack_` on an expression that is not an allocation is meaningless and causes type -error: +Placing `stack_` on an expression that is not an allocation is meaningless and +causes a type error: ```ocaml let f = ref (stack_ `Foo) ^^^^ @@ -116,7 +116,7 @@ to local, which prevents `l` from escaping the current region, and as a result the compiler will optimize `n :: x` to be stack-allocated in the current region. However, users may wish to use `stack_` to ensure stack allocation, as refactoring code can make an allocation that was previously on the stack -silenetly move to the heap. +silently move to the heap. ### Region vs. Scope From 195187d38e5a73bdb823d951b8cd63aa1d1da220 Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 11:50:35 +0000 Subject: [PATCH 20/21] minor fix --- jane/doc/extensions/stack/reference.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index a0dc4c94194..9aff440f5ce 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -203,9 +203,8 @@ let f (local_ x) = Both `x` and `y` are local and cannot, in general, escape a region. However, filling `??` in with `x` (but not `y`) is allowed. This is because we know that `x` lives outside of -`f`'s region and thus is guaranteed to be allocated in a pre-existing stack frame (or on -the heap) which will continue to exist after `f` ends. In contrast, `y` is a cons cell -allocated in the current stack frame, which will be destroyed after `f` ends. +`f`'s region and therefore will continue to exist after `f` ends. In contrast, `y` is a cons cell +in `f`'s region, which will be destroyed after `f` ends. ## Inference From 23a78f3234a3a526443c15511e450158715abc2d Mon Sep 17 00:00:00 2001 From: Zesen Qian Date: Mon, 24 Feb 2025 17:44:56 +0000 Subject: [PATCH 21/21] final fixes --- jane/doc/extensions/stack/reference.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jane/doc/extensions/stack/reference.md b/jane/doc/extensions/stack/reference.md index 9aff440f5ce..97a2e2700a7 100644 --- a/jane/doc/extensions/stack/reference.md +++ b/jane/doc/extensions/stack/reference.md @@ -208,8 +208,7 @@ in `f`'s region, which will be destroyed after `f` ends. ## Inference -In fact, the allocations of the examples above will be on -stack even without the `stack_` or `local_` keywords, if it is safe to do +In fact, allocations will be on stack even without `stack_`, if it is safe to do so. The presence of the keyword on an allocation only affects what happens if the allocated value escapes (e.g. is stored into a global hash table) and therefore cannot be stack-allocated. With the keyword, an error @@ -343,7 +342,7 @@ let f () = Here, since `g` refers to the local value `outer`, the closure `g` must itself be stack-allocated. (As always, this is deduced by inference, and an explicit -`stack_` or `local_` annotation on `g` is not needed.) +`stack_` annotation on `g` is not needed.) This then means that `g` is not allowed to escape its region (the body of `f`). `f` may call `g` but may not return the closure. This guarantees that `g`