Tail Calls and Local Regions in OCaml

A list-counting example reveals why a local closure can escape at a tail call, and how regional mode and [@nontail] affect the result.

채널
Jane Street
게시일
2024년 5월 7일
영상
15:59
읽기
4분
슬라이드
7
원문
영어
YouTube에서 열기
스크롤하면 영상이 오른쪽 아래로 작아져 계속 재생됩니다. 자막은 읽기 언어를 따릅니다.
읽기 언어2개 언어로 읽을 수 있음
이 글은 아직 한국어 번역이 없어 영어 원문으로 표시합니다.
요약

30초 요약

A Jane Street presenter demonstrates local mode in the organization’s OCaml compiler branch. The example explains tail-call optimization, local-region boundaries, the hidden regional mode and the stack-space tradeoff of disabling tail calls.

  1. 1An escaping local closureA local-list annotation exposes a tail-call error even though the outer function returns an integer.
  2. 2The region boundaryThe caller’s local region ends before a tail call, so its local closure cannot be used there.
  3. 3Use [@nontail]The annotation converts the site to an ordinary call, with an additional stack frame.
  4. 4The regional modeA value derived from a local parameter can retain a regional mode that permits a tail call.
  5. 5Recursive stack costsLosing regional information and disabling a recursive tail call can consume O(n) extra stack space.

핵심 정리

  1. A function’s local region ends immediately before a tail call.
  2. A closure capturing a local value must itself remain local.
  3. The [@nontail] annotation prevents tail-call optimization and keeps the region available for the call.
  4. Regional information permits some parameters to pass through a tail call without escaping their originating region.
  5. Disabling a recursive tail call can require O(n) additional call-stack space.
전문

챕터별로 다듬은 전문 읽기

원본 자막만 보기
01

Counting occurrences in a list

The presenter builds and tests a recursive count of the first element. Adding a local-list annotation reveals an escaping-value error.

This episode continues our series on local mode in the OCaml compiler branch being developed at Jane Street. I want to show how local values interact with tail calls, using a small list-processing example.

The example counts occurrences of the first element in a list. For [3; 1; 3; 8; 3; 3; 4], the result should be four. We will first implement it normally, then introduce locals and examine the resulting errors.

For an empty list, count_firsts returns zero. Otherwise, it binds the head as x and defines a recursive count_occs function over the tail. An empty tail returns one for the original head; each later match adds one, while a non-match continues without adding.

The recursive function closes over x, and the outer function calls it on the remaining list. Compiling and running the program prints four. The list in this particular example consists entirely of constants, so OCaml can avoid heap allocation already, but a general list might require it.

Counting the first list element
슬라이드 1 / 7Counting the first list elementThe recursive OCaml example counts matches while closing over the first element.

I annotate count_firsts to accept a local list and return an int. Compilation now reports that a local value escapes its region, with a hint that the function cannot be local because the call is a tail call. At first this is surprising: the returned integer cannot contain the list, and we have stored it in no reference.

A local value escapes its region
슬라이드 2 / 7A local value escapes its regionThe compiler highlights the tail call to the local closure.
02

Closures, regions and tail calls

A closure captures the local list head. Tail-call optimization ends the function’s region before the call, producing the error.

Pattern matching a local list makes x and xs local in this example. Because count_occs closes over x, its closure must also be local: putting it on the heap could leave a heap pointer to stack data after the local region ends.

Replacing the captured x with the constant 3 makes the error disappear, confirming that the captured local variable forces the closure to be local. But we actually want to count occurrences of x, so we need to understand the tail call.

If I bind the result of count_occs to a variable and then return that variable, the call is no longer in tail position and the program compiles and runs. The distinction comes from tail-call optimization.

Binding the result before returning
슬라이드 3 / 7Binding the result before returningThe refactoring makes the closure call non-tail.

When the last operation in a function is another function call, tail-call optimization can transfer control directly to the callee instead of returning to the caller first. The callee eventually returns to the original caller, avoiding a call frame that would have no further work to do.

A function’s local region ends immediately before its tail call, if it has one. Thus the local count_occs closure belongs to count_firsts’ region, but using it in the tail call would let it escape that region. Binding the integer result first works because the returned integer does not retain the closure or list.

03

Suppressing tail-call optimization

The [@nontail] annotation makes the call ordinary and adds a stack frame. The presenter then asks why a local-looking recursive argument can still be passed in tail position.

Marking the call [@nontail]
슬라이드 4 / 7Marking the call [@nontail]The annotation disables tail-call optimization for the call site.

The compiler branch provides an explicit alternative: annotate the call with [@nontail]. That prevents tail-call optimization at this site, so the local region remains available for the ordinary call and the program compiles.

This annotation changes runtime behavior: the ordinary call adds a call-stack frame. For this one call the speaker says the extra allocation takes very little time, but it is real additional stack usage.

There is a subtler question: why can count_occs make a tail-recursive call with ys, even though ys comes from a local list? Its local parameter is inferred correctly, and the call works without a nontail annotation.

04

Regional mode and recursive space

The hidden regional mode preserves information about values from outside the current region. An identity function demonstrates what happens when that information is lost.

A regional recursive argument
슬라이드 5 / 7A regional recursive argumentThe example marks the list tail derived from the parameter as regional.

The explanation is a hidden mode called regional, between global and local: global is a submode of regional, and regional is a submode of local. A local parameter comes from outside the current function’s region, so it may escape that one region. The tail ys obtained by matching that parameter is regional, allowing the tail call.

To demonstrate, I define lid as an identity function taking and returning a local value. Passing ys through lid and binding the result as zs loses the information that the value was regional: the type permits the call, but its result is known only as local.

Using zs in the tail-recursive call now produces the escaping-local error. Marking that call [@nontail] makes it compile, but doing so repeatedly in recursion can use O(n) more call-stack space for a long list. Preserving regional information avoids that cost for the tail-recursive branch.

Losing regional information
슬라이드 6 / 7Losing regional informationPassing the tail through a local identity function makes the recursive call require [@nontail].
05

Non-tail branches and remaining limitations

A call followed by addition is not in tail position. The presenter summarizes the rule and discusses the need for a better annotation experience.

The branch that adds one after the recursive call is different: 1 + count_occs zs is not a tail call, because addition still remains to be done. Passing a local value there is therefore accepted without a nontail annotation.

A call followed by addition
슬라이드 7 / 7A call followed by additionThe matching branch remains non-tail because it adds one after the recursive call.

The rule to remember is that the function’s region ends before a tail call. An explicit [@nontail] annotation, or binding the call result before returning it, prevents tail-call treatment at the call site.

In practice the current approach can require many nontail annotations. I think a better approach will be needed before upstreaming this work; there are ideas for heuristics, but their future remains open.

같은 채널의 다른 글

1:48:29EN
JSJane Street· 2026년 10월 16일How AI Is Changing Programming: John Krazit and Ron MinskyRon Minsky, John Krazit · Jane StreetJane Street’s AI tooling lead and the Signals and Threads host examine agent infrastructure, security, code review and the changing experience of writing software.코딩 에이전트Developer productivity평가(Evals)+2
1:36:37EN
JSJane Street· 2026년 9월 23일Aaron Bauer on Teaching Software Engineering in the Age of AIRon Minsky, Aaron Bauer · Jane StreetHow learning goals, active practice and editor design shape engineering education when models can generate the code.AI in EducationSoftware engineeringEducation+2
슬라이드 9장1:05:45EN
JSJane Street· 2026년 9월 11일Cyrus Omar on Live Programming and a Computational Commons for Humans and AICyrus OmarA demonstration of Hazel shows how immediate feedback, structured collaboration and carefully scoped permissions could support people and AI agents working together.Programming LanguagesHuman-AI Collaboration개발 워크플로와 테스트+2
1:25:00EN
JSJane Street· 2026년 9월 2일Eric Mannes on Turning Messy External Data Into Trading ResearchRon, Eric Mannes · Jane StreetFrom commodity futures and an Excel risk system to point-in-time datasets and AI, Eric Mannes explains the domain knowledge behind data engineering at Jane Street.Data quality & curation개발 워크플로와 테스트Investing+2
슬라이드 7장1:16:29EN
JSJane Street· 2026년 8월 26일Grant Sanderson and Alok Puranik: Positional Encodings Through Group TheoryGrant Sanderson, Alok Puranik · 3Blue1BrownStarting with assumptions about attention, they derive matrix exponentials and connect rotations, decay and Jordan blocks to RoPE and ALiBi.Transformers & attentionMathematics머신러닝 기초+1
27:39EN
JSJane Street· 2026년 8월 21일3Blue1Brown and Jane Street on Trading, Machine Learning and CultureGrant Sanderson, Nitya, In Young, Craig, Alok · 3Blue1BrownGrant Sanderson talks with Jane Street colleagues about market making, shared compute, research and the paths that brought them to the firm.Machine LearningFintechCompany Culture+2