Tessera: A New Era of Open-Source Earth Intelligence from Space

We've all seen satellites drifting by at night, and wondered how many are actually up there. With how long they have been observing Earth, it is unsurprising that satellites have accumulated a vast library of images covering the globe. In a world increasingly focused on identifying and mitigating human impact on the natural world, these images constitute an untapped data resource. Sadly, most conservation groups, field biologists, and local policymakers don't have a supercomputer on stand-by to help them trawl through that data and analyse trends. This is where the AI model Tessera, co-funded by Tarides and developed by a team headed by the University of Cambridge, comes in. What is Tessera? Tessera is an open-source AI foundation model which can parse through petabytes of satellite data to reveal patterns and changes over time. Tessera lets scientists designate habitat maps and highlight the changes they want to monitor over time. It can be used to reveal environmental changes, such as crop patterns, habitat loss, and deforestation. The software processes millions of satellite images from the European Space Agency into a compact, easily usable format for scientists. The result is that, instead of needing a supercomputer, researchers have access to global-scale analyses from their laptops. Tessera's algorithm learns to understand the 'fingerprint' of a landscape, knowing what it should look like, which allows it to fill in cloud-covered images with the missing values. For each 10-by-10-metre of land on Earth, the software creates an uninterpreted sequence of 128 numbers called 'embeddings'. All of these embeddings are available on demand in the database GeoTessera. A recent example comes from the Scottish Highlands, where a Cambridge-led team used aerial surveys to build detailed 3D maps showing the beginnings of large-scale native woodland recovery across the Cairngorms National Park. Tarides supported that work, and the data feeds into Tessera. You can read the University of Cambridge write-up. To learn more about the project, you can read the article published by the University of Cambridge, the open access paper, and watch the webinar on Tessera. Tessera & Tarides: A Natural Fit with our Work in Space We are proud to have sponsored Tessera's development, since it is open source and has no paywalls by design. As Tessera's co-lead, Anil Madhavapeddy, professor of planetary computing at Cambridge, puts it: the goal is to "democratise satellite data for people who lack massive computer resources, whether that's an underfunded conservation group here in the UK, field biodiversity researchers in the majority world, or planners figuring out climate adaptation strategies in a rapidly warming world." Tarides is dedicated to building secure and sustainable software with a positive impact, and supporting the Tessera project is a natural fit. It also complements our work on satellite software. Our sister company, Parsimoni, has been putting OCaml and MirageOS into orbit through SpaceOS, and we've built Notafs, a filesystem that lets Irmin run on MirageOS unikernels aboard satellites. Earlier this year, we also kicked off our part in the CEOS project, building next-generation Earth observation satellites. Tessera is a piece of that same puzzle; it's one thing to get reliable, secure software into orbit and collect data, and another to make sense of what that data is telling us once it's back on the ground. Whether it's the satellite hardware and unikernels collecting the imagery, or a foundation model turning that imagery into insight for a conservation group with no cloud budget, the focus for us is the same: open tools should make space and Earth science more accessible, not less. What's Next & Stay in Touch Explore Tessera's embeddings yourself at geotessera.org, and share your thoughts with the team behind the model. Stay in touch with us on Bluesky, Mastodon, and LinkedIn or sign up to our mailing list to stay updated on our latest projects. We look forward to hearing from you!

29 Oct 2026

Tarides

Read Article
OCaml now has type recovery: when type checking goes wrong, but keeps going

Merlin, the foundational library that underpins the reference implementation of the OCaml Language Server Protocol provides a wide range of practical features, including commands, code completion, and much more. It also implements more subtle mechanisms that are essential to delivering a smooth and responsive editing experience. Indeed, the editing environment for a programming language closely resembles the front-end of its compiler. However, whereas a compiler operates on complete, syntactically valid programs, an editor must continuously cope with programs that are incomplete, temporarily inconsistent, and often ill-typed. To handle these transient states, Merlin vendors a modified version of the OCaml compiler front-end (primarily the parser and the type checker), extending it with two recovery mechanisms: Parsing Recovery, which synthesises AST nodes to allow parsing to continue even in the presence of syntax errors. Typing Recovery, which synthesises typed AST nodes, allowing type checking to continue even when type errors are encountered. This article will focus specifically on the second mechanism, typing recovery, and how we upstreamed this feature to the mainline OCaml compiler. Typing Recovery in Action The recovery mechanisms' behaviour is easy to observe in practice. Consider, for example, the following OCaml program: let x = "ten" let y = x + 20 let z = (y + (2 * x)) && true Here, we are faced with three type errors. First, we attempt to add x and 20, which makes no sense since x has type string. Next, we attempt to multiply x (once again) by 2, which is also invalid. Finally, the expression (y + (2 * x)), which is expected to have type int, is instead used as a boolean value. When compiling this program, the OCaml compiler reports only the first type error in the compilation unit: File "lib/test.ml", line 2, characters 8-9: 2 | let y = x + 20 ^ Error: The value x has type string but an expression was expected of type int This is a very sensible design choice. Although OCaml has a highly sophisticated type system, the language's pervasive type inference means that a single mistake can easily trigger a cascade of secondary errors, obscuring the compiler's primary goal: ensuring correctness. However, applying the same strategy in a code editor raises two major (and closely related) issues: An editor that reported errors one at a time would provide a rather poor user experience. More importantly, if type checking or parsing stopped at the first syntax or type error, every feature provided by Merlin that relies on compiler structures – namely the parsetree and the typedtree – would immediately become unavailable. This would be particularly frustrating because, while actively editing a file, the program is almost always in a temporarily invalid state, whether syntactically, semantically, or both. For these reasons, the modified version of the OCaml compiler front-end embedded in Merlin always produces valid ASTs (Parsing Recovery) and typed ASTs (Typing Recovery), allowing the subsequent analysis phases to continue operating on programs that are still being written, whilst collecting errors: These two recovery mechanisms, although arguably among the most interesting features of the language server, are also one of the main reasons why upgrading Merlin to support a new OCaml release is such a challenging process. This effort is known as OCaml Release Readiness. For now, let's take a closer look at why this is the case by focussing specifically on Typing Recovery. Adapting Typing Recovery Across OCaml Versions Without going into too much detail, upgrading Merlin to a new OCaml version involves applying patches coming from two different sources: Compiler patches, corresponding to the various changes made to OCaml over time; Merlin patches, which introduce the recoveries. Because Typing Recovery is deeply intertwined with the various modules involved in OCaml's type-checking process, applying patches from two different sources (with different development cycles) makes version upgrades particularly tedious and introduces two major challenges: Each upgrade takes time, because the vendored version of the compiler front-end must be adapted while preserving all recovery information and fixing the analysis issues naturally introduced by the evolution of OCaml; Handling new recovery cases can be tedious for the people responsible for the upgrade process. Indeed, it is common to apply patches from the compiler side without having a complete understanding of their underlying rationale. For example, OCaml 5.5 recently introduced a new feature that was highly anticipated by the community: module dependent functions. This addition changes how function parameters are handled and requires careful consideration of how recovery should behave in these new situations. For these reasons, Typing Recovery could become unstable (within reasonable scenarios, of course), sometimes introducing small regressions and sometimes lacking support for newly introduced features. Reducing the Differences Between Merlin and OCaml Reducing the differences between Merlin's vendored version of the front-end and the one maintained by OCaml is therefore a crucial task. It not only helps reduce Merlin's maintenance burden, but also gives OCaml maintainers more control over how they want recovery to be handled within the type checker. Of course, this represents an additional responsibility for the maintainers (another one!), but in practice, developers introducing new features in the type checker usually have both a deep understanding of their implementation and a good intuition about how recovery should behave in the presence of errors. Integrating Typing Recovery into the Compiler At the time of writing this blog post, the Typing Recovery mechanism has been merged into the compiler. In this section, we will briefly describe its re-design and direct integration into the compiler's type checker. Typing Recovery has been part of Merlin since its very beginning. Although its implementation relies on very clever encoding, it has aged over time, and its underlying design has become increasingly difficult to understand. Integrating it into the compiler, therefore, provided an opportunity to rethink the mechanism, clean it up, and make its behaviour easier to understand. The goal was to enable a non-intrusive integration, giving maintainers the tools they need to confidently evolve and maintain it in the future. How Recovery Works, at a High Level The core of the OCaml type-checker is implemented as a set of recursive functions responsible for typing the different syntactic categories of the language, notably expressions, patterns, type declarations, modules, and classes. When a type error is encountered, these functions propagate an exception that interrupts the construction of the typing derivation. As mentioned above, the compiler adopts a first-failure reporting strategy, reporting only the first encountered error. To go beyond the first-failure reporting strategy, Merlin instruments the main recursive functions of the type-checker in order to intercept typing exceptions. When an exception is raised, it is caught so that it can be recorded (logged), and typing then resumes at the recursive call level (locally) on the other independent subterms of the program. Thus, even if the typing of a subexpression fails, independent subtrees can still be analysed, which makes it possible to report multiple typing errors within a single analysis. Resuming recursion allows new errors to be discovered, but it is not sufficient to produce an exploitable Typedtree. When a sub-derivation fails, Merlin synthesises an artificial Typedtree node whose typing judgment is that expected by the context in which it is inserted, while also preserving the fragments of the derivation that could be constructed before the typing failure, even if their attachment to the rest of the tree is no longer known. These nodes help preserve the structure of the Typedtree returned by the type-checker and maintain language server functionality despite the presence of errors. However, these generic mechanisms are not sufficient to cover all language constructs. Some phases of the type-checker perform preliminary checks before recursively typing their subterms. When these checks fail, recursion is never engaged, and no partial derivation is produced. For example, record expressions: { my_fst_field = e1; my_snd_field = e2 } The type-checker first attempts to resolve my_fst_field and my_snd_field. If, for instance, my_fst_field does not exist, it gives up without attempting to type e1 and e2. This strategy is suboptimal: subexpressions are never analysed even though they could contain useful information or independent errors. To address this issue, Merlin delays the error by assuming that the field exists. Merlin then temporarily adapts the type-checker by considering that an unknown label is not necessarily a fatal error, but may correspond to a field that will be added later. Typing then proceeds under this assumption, which makes it possible to produce partial derivations for subexpressions and to improve the quality of diagnostics. Another API for Raising Type Errors As the purpose of Typing Recovery is essentially to be managed by Merlin, we do not want to make it the default behaviour; it can therefore be enabled using a flag (-typing-recovery) passed to the compiler (which will be enabled by default by Merlin). In order to make error handling explicit, we introduce two functions intended to replace direct calls to raise in the type-checker. When the -typing-recovery flag is not enabled, these functions behave exactly like raise. In Typing Recovery mode, they record the exception in a dedicated structure, which will then be processed by the recovery handler. Typing_recovery.log_and_raise records the exception before re-raising it. It is used when processing cannot continue locally and captures the general case; Typing_recovery.log_or_raise records the exception without re-raising it when the context is able to perform local recovery, for example, by synthesising an artificial node or by continuing the typing of other subterms; it captures more fine-grained cases. This distinction makes explicit the points where recovery is possible and those where the exception must necessarily be propagated. The API guides the implementation of the type-checker and makes recovery decisions visible when adding or modifying typing rules. Today, we do not have a formal criterion to systematically determine which function to use. The integration into the OCaml type-checker was therefore guided by a large set of unit tests and empirical validation. In practice, the choice between the two functions depends on the expected behaviour of recovery, and is validated by observing the produced diagnostics. This lack of a general criterion aligns with the observations of the paper Merlin: A Language Server for OCaml (Experience Report), which notes that recovery policies would benefit from a more systematic theoretical framework. Forcing User-Facing Error Reporting In Merlin’s historical implementation, the selection of exceptions eligible to trigger a Typing Recovery scenario was mainly based on an empirical approach. The reimplementation in the compiler made it possible to identify a simple design invariant: only exceptions intended to be reported to the user (user-facing errors) should be recorded by the recovery mechanism. Conversely, internal type-checker exceptions, as well as simply re-raised exceptions (reraise), must not be captured. To introduce recovery as a systematic approach, we aim to channel user-facing exceptions so that they are always handled through a pair of functions log_and_raise and log_or_raise. To achieve this, we rely on a property of the OCaml language: exceptions are represented by the extensible type exn. This representation makes it possible to control the introduction of new exception constructors via module interfaces. By making certain exception constructors private in a module signature, it becomes possible to forbid their construction outside this module, while still allowing them to be deconstructed through pattern matching or exception handlers. This technique makes it possible to structure the flow of user-facing errors and to enforce a systematic passage through the associated logging functions. - exception Error of error + module Error : sig + type exn += private In_context of error + end = struct + type exn += In_context of error + + let log_or_raise err = + Type_recovery.log_or_raise (In_context err) + + let log_and_raise err = + Type_recovery.log_and_raise (In_context err) + end This transformation also makes it straightforward to rely on the compiler to guide the implementation by identifying and handling every possible recovery case individually and provide a precise API to handle recovery with or without resumption. Closing Words and Acknowledgements Although there are still some differences between Merlin's vendored front-end and the one maintained by OCaml, this change has already significantly reduced the number of Merlin patches that need to be applied. In the future, this will greatly simplify the process of upgrading to new OCaml versions. We recently provided a Merlin branch that anticipates the 5.6 release and relies on the new Typing Recovery mechanism! Although the next version of OCaml will expose the -typing-recovery flag to enable this mechanism, the goal is not for developers to use it directly, but rather for it to be leveraged by tools such as Merlin. Historically, Typing Recovery was designed and maintained by Merlin's two creators, Frédéric Bour and Thomas Refis. It was also maintained over the years by Ulysse Gérard. The integration into the compiler was funded by Jane Street. The review, conducted by Thomas Refis (who contributed greatly to knowledge transfer and design discussions), was funded by The OCaml Software Foundation. We would also like to thank Florian Angeletti for his quick review, guidance, and detailed explanations. This work is part of the broader effort to reduce the gap between OCaml and Merlin, and it is one of the most significant steps in that direction. We believe that the integration was carried out in a smooth and non-intrusive way, minimising the impact on maintenance work while drastically reducing the difficulty of future version upgrades. This will allow us to focus on delivering the best possible experience and continuously improve the OCaml code editing experience. Stay Tuned. Stay In Touch! Tried out the new update? Connect with us on Bluesky, Mastodon, Threads, and LinkedIn or sign up for our mailing list to stay updated on our latest projects. You can also connect with other OCaml users on Discuss. We look forward to hearing your thoughts!

15 Oct 2026

Tarides

Read Article
An Overview of OCaml Release Readiness: How Does the OCaml Ecosystem Stay Up-to-Date with the Compiler?

What is the OCaml ecosystem doing to make upgrading to the latest version of the language seamless? When the OCaml compiler receives a new update, it has the potential to have wide-ranging effects to the ecosystem of tools that surround it. In the past, users have hesitated to move forward with the latest release due to proper tooling support lagging behind. To improve the user experience and make transitions smoother, OCaml developers and maintainers agreed to work together to ensure that the core tools were always ready near release time. This effort is referred to as ‘OCaml Release Readiness’. It is a complex task spread across many actors in the ecosystem and supervised by Florian Angeletti (Inria) and Kate Deplaix (OCSF). In this post, we will give you a behind-the-scenes look at the work that goes into getting the OCaml ecosystem ‘release ready’. What are the ‘Core Tools’? There is no definitive rule for which tools are part of the release readiness effort, but, as a general guideline, it includes the kind of tools that provide key support in projects. If an update is incompatible with a core tool, it will be too disruptive for the user to migrate to the latest compiler version. Let’s look at the tools that are currently part of the release readiness cycle and what the maintainers do to get them up-to-date with new releases. Ppxlib Ppxlib is a library for building pre-processors in OCaml. Pre-processors are called at compile time and alter the program before compilation. They can perform a whole host of helpful functions, such as including a file, generating boilerplate, or extending the language. More or less half of the OCaml ecosystem depends (transitively) on Ppxlib. Internally, ppxlib has a copy of every abstract syntax tree (AST) of every supported compiler. Currently, that is every compiler version from 4.08 to 5.5. This is necessary because ppxlib has to migrate the ASTs of every user to a common AST (5.2 at the time of writing) to perform the pre-processing, and then migrate back to the AST of the user for the rest of the compilation process. For each new OCaml release, a new AST corresponding to the latest version is added to the migration chain, and transformations are written to and from that AST. If this AST includes new syntactic features or new representations of existing features, then the complexity of the task grows. The maintainers need to write encodings that preserve the new features and representations, particularly in the migrations to and from older ASTs. New Ppxlib releases are prepared by Nathan Rebours, via the OCaml Software Foundation (OCSF) and OCamlPro, and by Patrick Ferris at Tarides. Merlin Merlin is an editor service for OCaml that supports advanced IDE features. It is an analysis tool that provides semantic information about OCaml code, communicated to editors via a protocol such as LSP. A cornerstone of its architecture is the ability to recover from parsing and typing errors, so it can keep providing useful information on lines below the error rather than stopping. Achieving this requires patching large parts of the compiler's parser and typechecker in non-trivial ways, which makes upgrading Merlin's vendored parser and typer to each new compiler version a complex, time-consuming task. Several ongoing upstreaming efforts should make future updates much easier, like type-recovery which is scheduled to be upstreamed in the 5.6 version of the compiler, and the open proposal to upstream Merlin's new -short-paths implementation directly into the compiler as well, removing a long-standing source of drift between the two. The release readiness work for Merlin is undertaken by a team at Tarides. OCaml-LSP OCaml-LSP is the LSP frontend for Merlin. It has only minor dependencies on the compiler, so once Merlin's upgrade to a new OCaml version is done, upgrading OCaml-LSP is usually straightforward. The release readiness work for OCaml-LSP is done at Tarides. Dune Dune is a composable build system for OCaml. Its support for both monorepos and multi-package repositories has made it popular among OCaml developers. Its main branch tracks the latest compiler versions, and because it sits so early in the dependency graph, failures are quick to surface. In practice, the problems that come up are about compiling Dune itself with a newer compiler and not about the projects it builds. Strictly speaking this isn't even required, since a Dune binary built with an older compiler can still build projects with a newer one. But opam constrains a switch to a single compiler version for all packages in it, so in practice Dune has to stay compatible with the latest compiler releases. The release readiness work for Dune is done by a team at Tarides. OCamlFormat OCamlFormat parses and pretty-prints OCaml code. We need to update it every time new syntax is added to the language, which surprisingly happens in almost every OCaml minor release. OCamlFormat vendors two versions of the OCaml parser, so upgrading requires backporting patches with Git and resolving conflicts. We then update and test the printing code. The first parser is used purely for verification: OCamlformat parses the code before and after formatting, compares the two ASTs, and refuses to format if they diverge. The second parser is extended to keep more information about the concrete syntax. For example, these two snippets would have the same AST with OCaml's parser: [ 1; 2 ] and 1 :: 2 :: []. They are encoded differently with our extended parser. The release readiness work for OCamlFormat is done at Tarides. Odoc Odoc reads compiler-produced artifacts containing the abstract syntax tree of compilation units in order to generate module documentation. Since the type of those ASTs evolves between OCaml versions, odoc defines its own internal AST to be independent of the compiler version. Odoc uses cppo to adapt how the compiler's AST is loaded into odoc's internal representation at compile time. When a new OCaml version introduces changes to the typedtree, odoc fails to compile until it's adapted. At minimum, this means updating the loading logic to handle the new types while staying compatible with older versions via cppo conditionals. Beyond that, odoc's internal AST may need to be extended to represent new features, and add the logic to support rendering the changes in the generated API documentation when applicable. The release readiness work on odoc is done at Tarides. Stay in Touch Did you know how much work goes on behind-the-scenes to keep the OCaml ecosystem’s tools up-to-date with new compiler versions? Connect with us on Bluesky, Mastodon, Threads, and LinkedIn or sign up for our mailing list to stay updated on our latest projects. We look forward to hearing from you!

07 Oct 2026

Tarides

Read Article
Combobulate Now Supports OCaml & OxCaml

Code navigation is one of those aspects of programming that can either make your experience significantly better, or be such a pain. Most of the time we navigate code as text, i.e, searching with regexp, jumping by lines or moving word by word. But code isn't text. It has syntactic structure, and being aware of that structure when moving and editing opens up a different way of working. This is what structural navigation and editing means: operating on the actual constructs of a program; expressions, bindings, match arms, module definitions, etc, instead of characters and lines. We have recently improved navigation in OCaml/Oxcaml with Combobulate support, and this post will get you up-to-speed on what’s new, how it works, and where to try it out! What Has Structural Navigation in OCaml Looked Like Until Now? OCaml already has a substrate of structural navigation through Merlin (and by extension OCaml-LSP). The jump command gives you a limited form of structural movement such as jumping to the next let, match, module, and a few other constructs. It is useful, but it's a small subset of what structural navigation could be. Previously, this limitation motivated the GopCaml project, which took a more ambitious approach to structural editing for OCaml by working directly with the compiler's AST. More recently, tree-sitter has introduced a generic abstraction over syntax. Given a tree-sitter grammar for a language, you get an incremental parser that produces a concrete syntax tree you can query and traverse. OCaml has a tree-sitter grammar which is already used in, for example, neocaml-mode where it provides syntax highlighting. Tree-sitter can be seen as the syntactic counterpart to LSP: where LSP standardizes semantic features, Tree-sitter provides a common protocol for syntax. Much like TextMate grammars provided a generic way to handle syntax highlighting across editors, Tree-sitter gives editors syntactic tools such as highlighting and navigation. Combobulate by Mickey Petersen takes tree-sitter in a different direction: it uses the syntax tree for structural navigation and editing. It's a minor mode for Emacs that supports many languages, and it now supports OCaml. Why Does Combobulate Matter for OCaml? OCaml code nests very deeply. Modules contain structures, structures contain let bindings, let bindings contain match expressions, and match cases can contain further match expressions. Type declarations can define records, variants, and GADTs in a single type ... and ... block. Many of these constructs can recurse into each other with no fixed limit; this is part of what makes OCaml expressive, but it also means that even a small OCaml file produces a deep and wide tree-sitter parse tree. Implementing structural navigation for OCaml is harder than for most languages precisely because of this: the procedures that tell Combobulate how to pick the right node at any point have to account for potentially infinite nesting at every level. This is also why line-based movement becomes incredibly slow and unreliable. Jumping to the next let with an incremental search won’t help when there are six of them nested within each other. This is why structural navigation, which helps us move by the structure of the code, and the relationships between different nodes in the tree, feels natural and makes a real difference. Combobulate is an important addition to the OCaml ecosystem because it perfectly complements tools like Merlin and OCaml-LSP. While Merlin is great for semantic intelligence, type checking, autocomplete, and jumping to definitions, its structural navigation features (like the jump command) are limited. By letting Combobulate handle the purely syntactic, structural movement and editing, the two tools work together to provide a comprehensive editing experience: Merlin understands what your code means, while Combobulate understands its shape. Navigating OCaml with Combobulate Once Combobulate is active in your OCaml buffer, you should see a © in the mode line. There is a Magit-style transient UI bound to C-c o o that lists every binding, which is handy while you're learning. To inspect the full keymap directly, run M-x describe-keymap RET combobulate-key-map. With Combobulate, you have different commands to navigate your code in a variety of ways: jumping between siblings, jumping between occurrences of words, traversing the node tree sequentially, and more. Navigation Commands Binding Summary What it does C-M-u / C-M-d Up/Down into list Move in/out to the parent/child node. C-M-n / C-M-p Forward/Backward sibling Move to the next/previous sibling at the current level. M-e / M-a Logical next/previous Jump to the next/previous logical node, regardless of nesting. M-n / M-p Sequence navigation Move between paired sequence points (e.g., jumping from the word let to the next occurrence of let). C-M-a / C-M-e Move to the start/end of defun Move to the beginning/end of defun. This is based on best-effort. In nested let bindings, it doesn't work very well. Navigation Examples Combobulate primarily handles code navigation in terms of two axes: Vertical/Hierarchical (Parents and Children): Moving "up" (C-M-u) leaves the current node for its enclosing parent, while moving "down" (C-M-d) descends into the child node at the cursor. Horizontal (Siblings): Moving forward (C-M-n) or backward (C-M-p) hops between sibling nodes at the same syntactic level, such as adjacent match cases, list elements, or record fields. When hierarchical or sibling navigation isn't enough, Combobulate also offers logical navigation (M-e / M-a). Rather than being constrained to direct parent-child or sibling relationships, logical navigation moves sequentially across nodes in their logical reading order—allowing you to cross operator boundaries or escape deeply nested subtrees. Simple Examples Navigating down into a body (C-M-d) "Down" means entering whatever node the cursor is sitting on. The clearest case is descending from a module declaration into its contents: module Counter = struct let value = 0 let bump x = x + 1 end Place the cursor on module. Press C-M-d thrice and the cursor moves to let value = 0. Press C-M-d again and you descend further, into the binding itself. Navigating up to the parent (C-M-u) "Up" is the inverse: leave the current node and land on its enclosing parent. Suppose the cursor is on the number 100 inside a record: let player = { name = "Ada"; score = 100 } C-M-u jumps to the whole field score = 100. Press it again to land on the record { ... }. To move from 100 directly to the let keyword, use C-M-a. Navigating siblings (C-M-n / C-M-p) Siblings are nodes at the same level, like match cases, tuple components, record fields, and array elements. Take a match expression: match shape with | Circle r -> pi *. r *. r | Square s -> s *. s | Triangle (b, h) -> 0.5 *. b *. h Place the cursor on the first match arm (Circle r -> ...). C-M-n moves to Square s -> .... Again to Triangle .... C-M-p walks back. Complex Examples Using only parent-child or sibling navigation is not always sufficient to navigate OCaml code efficiently. Because OCaml's deep nesting can lead to highly nested concrete syntax trees, you need a few more tools in your belt to avoid getting stuck. Example 1: Using next-sequent (M-n) and prev-sequent (M-p) In subsequent let...in bindings, parent-child/sibling navigation is insufficient and unreliable due to how let...in is represented as deeply nested subtrees in the tree-sitter grammar. Each successive binding is actually a child of the one before it, meaning C-M-p won't walk backwards up the chain. Instead, use sequence navigation to hop directly from one let to the next and back. let emit_string_table_section fmt section_name (table : Dwarf_write.string_table) = let buf = Buffer.create 64 in let contents = Buffer.contents buf in let i = ref 0 in let len = String.length contents in while !i < len do let start = !i in while !i < len && contents.[!i] <> '\x00' do incr i done; let s = String.sub contents start (!i - start) in emit_asciz fmt s; if !i < len then incr i done If we want to move from the let-binding on line 3 to the let-binding on line 6, sequence commands M-n and M-p let you jump forward and backward easily. Example 2: Using logical-next (M-e) and logical-prev (M-a) if (x = 1) then true else false When the cursor is on if, you can do C-M-d to go to the parenthesis (, then C-M-d again to enter x, or C-M-n to go to then and else. However, if we have the same code without the parenthesis: if x = 1 then true else false There is no direct sibling relationship to go from x to then using C-M-d or C-M-n. In this case, we use logical-next (M-e) to cross the operator boundary and jump directly to the then branch. Logical next/prev allows you to move to the next node in the tree irrespective of their parent/sibling relationships. It is also incredibly helpful for passing over ->, =, and other operators. Example 3: Escaping Deep Subtrees If you are at the end of a long top-level item and want to navigate to the beginning of the next top-level item, use logical-next (M-e). If you try to use forward sibling navigation (C-M-n) from the end of the item, the cursor won't move at all since you are deep inside a nested subtree with no siblings to your right. Using M-e lets you jump out of the subtree instantly to the next top-level construct. Editing Commands Because Combobulate's editing commands are built on top of its navigation primitives, particularly sibling navigation, they all work in OCaml without any extra configuration. If you can navigate between two nodes, you can edit them. Binding Summary What it does C-c o e Envelope prefix Apply a code template (envelope) at the cursor. Press C-h after to see what's available in this context. M-h Expand region Mark the current node. Repeat to expand the region to the parent iteratively. C-M-h Mark defun Mark the current enclosing defun. Repeat to expand to the next enclosing defun iteratively. M-N or M-S-n Drag forward Swap the current node with its next sibling, preserving formatting. M-P or M-S-p Drag backward Swap the current node with its previous sibling. C-c o c Clone node dwim Duplicate the node at cursor. If ambiguous, you cycle through candidates with a live preview (the carousel). C-c o t Place cursors Place multiple cursors (or field-editor fields) at every related sibling; e.g. each element of an array, each field in a record. Editing Examples Expanding the region (M-h) Each press grows the selection to the next syntactic unit. Starting on r inside a function call: let area = pi *. r *. r M-h once → selects r. M-h again → selects pi *. r *. r. M-h again → selects the whole let binding. M-h displays numbers indicating where the next enclosing region starts, helping you visualize where the cursor will move if you perform a hierarchy-up navigation. Unlike Merlin's type-enclosing (which operates on typed AST expressions and requires code to typecheck), Combobulate's expansion is purely syntactic: it operates on any concrete syntax node (including patterns, type declarations, and comments) even when the code is incomplete or doesn't compile. Expanding an envelope (C-c o e) Envelopes are context-aware templates. Press C-c o e then C-h to see what's available. For example, to add a module template: Place your cursor where you want to add the template. Press C-c o e to list all available templates. Press M to activate the modules template. The template will be added with name as an editable hole: module name = struct end Press TAB to jump between holes. Adding multiple cursors (C-c o t) Cursors land on every sibling at the current level. This is perfect for bulk-editing collections. Place the cursor on any element of an array: let primes = [| 2; 3; 5; 7; 11 |] Press C-c o t t and a cursor is placed on each element. Anything you type happens to all five at once! Swapping siblings — drag forward / backward (M-N / M-P) Drag transposes the node at the cursor with its neighbor, preserving formatting. Useful for reordering elements or record fields: let primes = [| 2; 3; 5; 7; 11 |] With the cursor on 2, press M-N (or M-S-N) to swap them: let primes = [| 3; 2; 5; 7; 11 |] Cloning a node (C-c o c) Duplicates the node at the cursor. On a record field: type user = { name : string; age : int; } Place the cursor on name : string and press C-c o c to duplicate it seamlessly. Inspection & Search Binding Summary What it does C-c o B q Query builder Open the interactive tree-sitter query builder, with completion and highlighting, for ad-hoc searches and bulk edits. Query Builder Example Open a live tree-sitter query builder with C-c o B q. If you have value_definitions in your file, you can underline all of them with a blue line using the query: (value_definition) @hl.blue.underline Setup Since Combobulate is built on tree-sitter you will need Emacs 29 or later, as that's when built-in tree-sitter support landed. Install Combobulate from the master branch and add the OCaml grammars to your config file. To get started with OCaml, add the OCaml grammars to your config file: (setq treesit-language-source-alist '((ocaml . ("https://github.com/tree-sitter/tree-sitter-ocaml" "v0.26.0" "grammars/ocaml/src")) (ocaml_interface ("https://github.com/tree-sitter/tree-sitter-ocaml" "v0.26.0" "grammars/interface/src")))) Run M-x treesit-install-language-grammar for each. Combobulate can be used with either neocaml-mode or tuareg-mode or tuareg-interface-mode as your major mode. When it's working you'll see © in the mode line, and C-c o o opens the full command palette. Try it out Open up a project you are working on. Place your cursor on a case in a match expression and try to teleport to the next sibling. You can check out the PR adding OCaml support and the PR adding OxCaml support in the Combobulate repo to explore the implementation process in more detail. Feedback Welcome OCaml's syntax is flexible enough that there isn't always one obvious answer to "what should the next sibling be?" or "what counts as descending one level?". We had to make judgment calls on a number of corner cases, like what sibling navigation does inside a type ... and ... block, how hierarchy behaves around functors, where sibling navigation should land in deeply nested expressions. We're happy with the choices we made, but we know they won't match everyone's expectations perfectly. If something feels off in your workflow, or you think a particular movement should behave differently, we'd like to hear about it. Open an issue on the Combobulate repo, make a post on Discuss, or contact us to let us know. Stay in touch with us on Bluesky, Mastodon, and LinkedIn or sign up to our mailing list to stay updated on our latest projects. We look forward to hearing from you!

01 Oct 2026

Tarides

Read Article
Formal Verification: Release of Ortac 0.8.0 for Testing with Domains

Ortac 0.8.0 generates QCheck-STM test suites that check OCaml libraries for parallel safety with multiple SUTs. Here are the design choices behind it.

16 Sep 2026

Tarides

Read Article
Exploring Static Data Race Freedom in OxCaml

Discover the data-race-freedom modes of OxCaml, and how we tested them on a multicore branch of OCaml's editor service Merlin!

15 Jul 2026

Tarides

Read Article