Posts in Ocaml (20 found)

Why I think Rust is Object Oriented

Before we begin, I want to say I don't care that much if you disagree with me. There's no sound precise mathematical definition of Object Oriented, no ISO standard, and no grand arbiter who decides what is and what is not OO (although I believe Casey Muratori may have applied for that position somewhere in his 12 day monologue about the subject). You are unlikely to change my mind, and I am unlikely to change yours, and that's perfectly fine. On with the post! In my heart of hearts, Rust feels like an object oriented language. Practically everything I write in Rust is a datum + set of behaviours intimately associated therewith. I hide my struct fields, give my totally-not-objects equality semantics and string representation. I utilise polymorphism via traits - and while I know that traits are not technically interfaces, that fact occupies the same region of my brain that knows that mandrills are not technically baboons. Almost all my functionality is written in methods, and those methods belong to the data they operate on, in a way that they definitely don't in something like C or OCaml. But what about inheritance? OO-haters often fixate on this as the defining thing about objects, which has always perplexed me. I came up in the JavaMania era and "composition over inheritance" was the accepted wisdom of every programmer who took OO seriously. I scarcely used it during my time in the C# mines. Historically speaking the case is weak as well; neither the first smalltalk, nor the first simula had inheritance. Self was hugely influential (traits originated in Self) and didn't have it either. I won't deny it wasn't a common feature, but it never took center stage in my mind outside the brief "Cat inherits Mammal" phrase we all go through, and it certainly wasn't encouraged in the OO design books I read. So why the disconnect? Why does talking about this not resonate with other Rust programmers? I think because in the Rust culture, "objects" are something very different. They're virtual destructors & inheritance trees. They're a nasty thing C++ had that Rust forwent because dynamic dispatch is slow (except in Zig of course where it's fast now). Of course Rust isn't OO! But for those of us who took object-oriented design seriously, everything you read just reinforced that they're neat little black boxes you call methods on. And of those, rust is full.

0 views
Farid Zakaria 2 weeks ago

A TacoSprint 2026 Retrospective

This is my retrospective of TacoSprint 2026 that took place in June 2026 at La Saladita, Guerrero, Mexico. For a while now, I have watched from the sidelines as Nixers around the world gathered for sprints: OceanSprint , ThaigerSprint , SaltSprint , TransylvaniaSprint , AuroraSprint and NixCamp . Wow, we Nixers sure do like our sprints! All of them also happen to be in Europe or East-Asia. With an upcoming fourth baby on the way, I figured it was now-or-never to put words into action. I messaged @domen , who organizes OceanSprint every year, and asked if he’d be interested in helping me set up the first sprint in North America. Domen is an avid surfer, a recurring theme in his OceanSprints, so I appealed to his inner-surfer and we spec’d out some places in North America that were both cost-effective and had ample, amazing surf 🏄. I had already been to the Troncones and the La Saladita area, so my prior experience removed a large vector of the unknown. It seemed like a no-brainer. Domen was already going to be in South America in June, so the timing lined up nicely (+ summer is the swell season there!). We set to work standing up a website and trying to attract sponsorship and attendees. This was probably the hardest part of organizing a brand-new sprint. We had far lower turnout for registration and sponsorship than I foresaw. Several people responded on our application form, or told us directly, that they were unsure about the safety of visiting Mexico, since the US Department of State had it under a travel advisory. Despite my best efforts to soothe everyone’s fears, it remained a real hindrance. Note For those still on the fence for next year: the area felt extremely safe. We rented a house in a fairly secluded stretch that caters almost entirely to surfers. At no point did anyone feel uncomfortable or unsafe. Getting there was its own small adventure. Flights were unusually challenging to book thanks to the World Cup soaking up demand across the region. The most dramatic casualty was Alex ( @adeci ), who managed to completely miss his connecting flight and arrived three days late. To his credit, he showed up in great spirits and slotted right back in to hacking with the group like nothing happened. Once everyone was settled, we fell into a rhythm that I can only describe as suspiciously sustainable : It was amazing to bookend each day with a surf at La Saladita’s left point break. Surfing for me lets me enter flow state very similar to when I am deep in thought hacking-away. It helped clear through a lot of built-up gunk and I often returned back with a clear intention or solution to a problem I had been working on. One of the more unexpectedly wonderful parts of the trip was the meal preparation from Gladys, our local cook, who pretty much cooked for us three times a day. We were extremely well-fed, which let us focus on the Nix-hacking and motivated me to make sure I kept up with the surfing to put off any weight gain 🫠. The website will be updated to have a more formal summary of every contribution we managed to put forward and their current status however it was amazing to see how much work a group of nine people can put forward in a single week with a combined mission and passion for an ecosystem. Our work spanned dynamic linking, package relocatability, peer-to-peer remote builds, faster module systems, shrinking the OCaml runtime closure and cross-distribution packaging. A few of my own threads, if you want to go deeper: LLM-based agents featured prominently throughout. We were fortunate to have Geoff Huntley with us, who is quite the AI-maximizer , spiritually guiding us and offering us some SOTA insight in how we might want to explore leveraging AI. Alan ( @alurm ), had the greatest idea for us to put together an academic style trip report. We worked together on the paper and the result is Attention, Nix and Tacos Is All You Need , a loving parody of a certain famous paper. An arXiv submission is coming, but in the meantime you can read it below or download it here . Your browser doesn't support embedded PDFs. You can download it here instead. We already agreed to organize the same sprint next year. I can’t wait. This was literally the most enjoyable thing I’ve ever done as it combined my two passions (surfing & hacking) in a way I honestly did not think was possible all while producing a ton of value to the Nix ecosystem. For a different vantage point, please check out the retrospectives from my fellow attendees! GuixPkgs: every Guix package, as a Nix flake Hijacking ELF entry points for NixOS compatibility, or wtf is wrap-buddy Nix needs relocatable binaries Alan Urmancheev Jared Siegel

0 views
(think) 3 weeks ago

Neocaml 0.9: A Better REPL, Dune/Opam Completion, and More Robustness

It’s been a couple of months since the last neocaml release, and the reason is simple — for a while there I was genuinely out of ideas. Back when I shipped 0.6 I declared (again!) that I was done with new features, and this time I almost meant it. But ideas have a way of creeping back in, and 0.9 turned out to be a meaty release. Here are the highlights. The biggest chunk of work went into the REPL (toplevel) integration. I’m well aware that the OCaml toplevel isn’t terribly popular with seasoned OCaml developers — most of them reach for a proper build and a debugger instead. But I think newcomers get a lot of mileage out of a REPL, and (no surprise to anyone who’s followed my work) I’m a Lisper at heart with a real soft spot for interactive development. Clojure and Emacs Lisp spoiled me, and I want OCaml beginners to taste a bit of that too. So, what’s new: Choose your toplevel. The new lets you pick between , , and . Set it globally, or per project via : The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to. This one I’m particularly happy with. / files have to lean on for completion, but the auxiliary file formats have no language server at all. That’s exactly the kind of gap neocaml is meant to fill, so both and now ship a backend. In a file you get completion for stanza names, the field names valid for the enclosing stanza, and library names inside / fields: The library candidates combine your project’s own libraries with whatever’s installed in the active Opam switch. And it’s switch-aware — if there’s a project-local switch (an directory), neocaml detects it and queries it via , without any configuration on your part. The results are cached per project, so it stays snappy. In files you get completion for field and section names, and package names inside / / (sourced from ): Both backends can be toggled off via and if you’d rather not have them. A good chunk of this release is the unglamorous but important work of making things just behave correctly: There’s more in there too — integration ( ), clickable URLs and bug references in comments, a font-lock level selector, and richer menus across the modes. OCaml 5.5 was released on June 19th, so this felt like a good moment to ship 5.5 support in neocaml. The and grammars now track tree-sitter-ocaml v0.25.0, which brings the 5.5 grammar along with it. There’s a wrinkle here that’s worth explaining, because it’s shaped the last few releases. A tree-sitter grammar gets compiled into a parser that speaks a particular ABI version , and Emacs can only load parsers up to the latest ABI supported by the it was built against — it’s not the Emacs version itself that sets the ceiling. In practice a lot of Emacs 30 builds out there (notably Homebrew’s on macOS) are linked against tree-sitter 0.24, which tops out at ABI 14 ; you need an Emacs built against tree-sitter 0.25+ to load ABI 15 grammars. The trouble is that the tree-sitter 0.25 CLI now generates ABI 15 parsers by default, so any grammar regenerated with current tooling produces something those builds simply can’t load — you install it and it just errors out. Emacs 31 will ship with newer tree-sitter and make ABI 15 the common case, but it’s not out yet. (This isn’t a neocaml problem as such; it’s been biting tree-sitter modes across the ecosystem.) After a few users ran into exactly this, I’ve made a deliberate decision: stick to ABI 14 grammars until Emacs 31 is widely available. That effort started a couple of releases back — in 0.8.1 I lowered the ABI requirement from 15 to 14 across the opam, dune, and ocamllex modes, switched the menhir recipe to tmcgilchrist/tree-sitter-menhir , and pinned ocamllex back to v0.24.0, all of which target ABI 14 ( #42 ). 0.9 extends that policy to the core OCaml grammars. The catch with v0.25.0 is precisely that it generates an ABI 15 parser. Happily, the 5.5 grammar didn’t actually need any ABI 15 features — the bump rode along with the CLI upgrade — so an ABI 14 regeneration of the very same grammar is a drop-in. Big thanks to 314eter , the maintainer, for cutting a tag for exactly this purpose ( #141 ). The one snag was that tagging normally triggers releases to NPM, crates.io, and PyPI, so I sent a small PR to skip publishing for ABI-suffixed tags ( #142 ), and the tag followed. neocaml now pins both grammars to it. If you’re curious where neocaml is headed, I’ve started keeping a ROADMAP.md with ideas and guiding principles (short version: tree-sitter first, lean on the LSP stack for / , and own the auxiliary modes that have no language server). The project also has a proper documentation site now at neocaml.org , so there’s a real home for the details beyond the README. As always — update from MELPA , play with it, and let me know how it goes. The full list of changes is in the 0.9.0 release notes . Bug reports, feature requests, and pull requests are all welcome on GitHub . That’s all from me, folks! Keep hacking! A dedicated REPL per project. The REPL buffer is now named after its project (e.g. ), and the send commands route to the current buffer’s project REPL. You can have several projects running side by side without them stepping on each other. Choose your toplevel. The new lets you pick between , , and . Set it globally, or per project via : The active flavor shows up in the REPL’s mode line, so you always know what you’re talking to. Send a phrase and step. ( ) sends the phrase at point to the REPL and moves on to the next one — perfect for walking through a file top to bottom while you experiment. from Emacs. loads a package into the running toplevel without you having to type the directive by hand. Restart on demand. kills and restarts the toplevel when things get into a weird state. Character literals and quoted strings at the syntactic layer. Tree-sitter fontifies , , and correctly, but the syntax table underneath was getting confused — which broke , , and around those constructs. The fix was a good old . I wrote up the whole story over on Emacs Redux in Tree-sitter Modes Still Need a Syntax Table , if you’re into mode-writing internals. integration. A directory with a file is now recognized as a project root (even without version control), and are ignored, and defaults to in dune projects.

0 views

Flat/Non-higher-order construction of ANF via mutable holes

This post will describe an algorithm for constructing an ANF/similar IR from a functional-like base language with no closure usage, and with no related excess space requirements. This algorithm is as far as I know not new, and has apparently been known in imperative compilation for a while. However, I admittedly have no source for this, nor have I seen it described for functional-like languages. Thanks to Ryan Brewer and rpjohnst for their inspiration on this. If you are only interested in code, please skip to Implementation. First, let us describe our input and output languages. Our input language will look like this: This is meant to echo some simple functional programming language, and contains all the elements needed to demonstrate the algorithm. Our output language will look like a lexically-scoped flat ANF-style construction, with all lets at the toplevel. The goal is to convert from our input to this language. The notable addition is the constructor in , the purpose of which will be described now. Imagine we are trying to convert the term When we arrive at the body of , we have a conundrum. We need to put the conversion of before , so that using is well-scoped. Hence, we must convert before we can continue. How do we use this conversion, though? 's content needs to be put in the field of 's , so it's not clear how to both convert before and to use 's finished conversion in . One "traditional" method of solving this is via continuations, where is passed a closure representing "what to do next", after it converts its body. We can then put 's content in that closure, which will be invoked later. This has two main flaws: This method resolves these. Recall that we left a field in our type. I will denote a hole like this: . The central idea is that our conversion of any given fragment will return: Consider this simplified example: When we convert , this signals for the conversion of . There are two steps to converting a - we must first convert the "head" ( ), and then the "body" ( ). Converting the will result in something of form Note that the hole is left to be filled later. This way, the conversion of 's head need know nothing about its environment. We represent this in code via the constructor, which contains a reference to a fragment that either may be empty ( ) or filled ( ). The conversion of the body can then proceed by first generating the expected addition, binding it to so it may be referenced later: We now need to get this fragment into the proper lexical scope. We can do this by plugging it into the hole left by the conversion of the head: Then, returning this fragment, the name bound ( ), and the reference to the inner hole, we can continue converting ; First, convert the head, using the name returned by the conversion of : Then, the body: And finally plug it into the hole returned by . We are left with a final hole we can plug with to be explicit about the end of a control flow path. One disadvantage of this strategy is that it does generate many unneeded names. Most of these can be avoided by carefully special casing certain , and the rest can be eliminated via a simple folding pass. Below is a sample implementation of this algorithm, in OCaml. I recommend toying with the conversion yourself, using or similar. In the following, I will "clean up" the output slightly, as to not make it unreadable. If we use our first example term: We can then plug the returned hole, as described. Via some inspection, we can verify this is indeed correct, and conforms to our ANF-like form, as expected. As mentioned earlier, the large amount of duplicate names can be mitigated through strategies such as special casing and folding passes. Clearly, the algorithm does not use closures, or any indirect jumps. It does not allocate any more than needed for the representation of the output program; there is no excessive closure allocation. I do not currently have a good way to test performance on real-world large inputs, but I have no reason to believe it would not perform better than an equivalent higher-order version. I believe this method should be extendable to CPS as well, although I have not tried as of the moment. The "usual" higher-order CPS conversion described in e.g. "Compiling with continuations, continued" looks quite similar to the higher-order presentation of ANF conversion, so I would be interested in whether it can be similarly massaged into a mutable form. It uses excess space. A conversion of a term will allocate ~2x the memory "needed"; it must allocate for the ANF construction itself, and for the closures needed during the construction. It is slower, due to the aforementioned closure allocation and application. The fragment itself. A name we can use to refer to what was bound in the fragment (In the conversion of addition/similar with nested content, names must be conjured.) The reference created by the hole, so that it may be filled.

0 views
(think) 3 months ago

fsharp-ts-mode: A Modern Emacs Mode for F#

I’m pretty much done with the focused development push on neocaml – it’s reached a point where I’m genuinely happy using it daily and the remaining work is mostly incremental polish. So naturally, instead of taking a break I decided it was time to start another project that’s been living in the back of my head for a while: a proper Tree-sitter-based F# mode for Emacs. Meet fsharp-ts-mode . I’ve written before about my fondness for the ML family of languages, and while OCaml gets most of my attention, last year I developed a soft spot for F#. In some ways I like it even a bit more than OCaml – the tooling is excellent, the .NET ecosystem is massive, and computation expressions are one of the most elegant abstractions I’ve seen in any language. F# manages to feel both practical and beautiful, which is a rare combination. The problem is that Emacs has never been particularly popular with F# programmers – or .NET programmers in general. The existing fsharp-mode works, but it’s showing its age: regex-based highlighting, SMIE indentation with quirks, and some legacy code dating back to the caml-mode days. I needed a good F# mode for Emacs, and that’s enough of a reason to build one in my book. I’ll be honest – I spent quite a bit of time trying to come up with a clever name. 1 Some candidates that didn’t make the cut: In the end none of my fun ideas stuck, so I went with the boring-but-obvious . Sometimes the straightforward choice is the right one. At least nobody will have trouble finding it. 2 I modeled directly after , and the two packages share a lot of structural similarities – which shouldn’t be surprising given how much OCaml and F# have in common. The same architecture (base mode + language-specific derived modes), the same approach to font-locking (shared + grammar-specific rules), the same REPL integration pattern ( with tree-sitter input highlighting), the same build system interaction pattern (minor mode wrapping CLI commands). This also meant I could get the basics in place really quickly. Having already solved problems like trailing comment indentation, hybrid navigation, and with qualified names in neocaml, porting those solutions to F# was mostly mechanical. The initial release covers all the essentials: If you’re currently using , switching is straightforward: The main thing doesn’t have yet is automatic LSP server installation (the package does this for ). You’ll need to install FsAutoComplete yourself: After that, is all you need. See the migration guide in the README for a detailed comparison. Working with the ionide/tree-sitter-fsharp grammar surfaced some interesting challenges compared to the OCaml grammar: Unlike OCaml, where indentation is purely cosmetic, F# uses significant whitespace (the “offside rule”). The tree-sitter grammar needs correct indentation to parse correctly, which creates a chicken-and-egg problem: you need a correct parse tree to indent, but you need correct indentation to parse. For example, if you paste this unindented block: The parser can’t tell that is the body of or that belongs to the branch – it produces ERROR nodes everywhere, and has nothing useful to work with. But if you’re typing the code line by line, the parser always has enough context from preceding lines to indent the current line correctly. This is a fundamental limitation of any indentation-sensitive grammar. OCaml’s tree-sitter-ocaml-interface grammar inherits from the base grammar, so you can share queries freely. F#’s and grammars are independent with different node types and field names for equivalent concepts. For instance, a binding is in the grammar but in the grammar. Type names use a field in one grammar but not the other. Even some keyword tokens ( , , ) that work fine as query matches in fail at runtime in . This forced me to split font-lock rules into shared and grammar-specific sets – more code, more testing, more edge cases. F# script ( ) files without a declaration can mix bindings with bare expressions like . The grammar doesn’t expect a declaration after a bare expression at the top level, so it chains everything into nested nodes: Each subsequent ends up one level deeper, causing progressive indentation. I worked around this with a heuristic that detects declarations whose ancestor chain leads back to through these misparented nodes and forces them to column 0. Shebangs ( ) required a different trick – excluding the first line from the parser’s range entirely via . I’ve filed issues upstream for the grammar pain points – hopefully they’ll improve over time. Let me be upfront: this is a 0.1.0 release and it’s probably quite buggy. I’ve tested it against a reasonable set of F# code, but there are certainly indentation edge cases, font-lock gaps, and interactions I haven’t encountered yet. If you try it and something looks wrong, please open an issue – will collect the environment details for you. The package can currently be installed only from GitHub (via or manually). I’ve filed a PR with MELPA and I hope it will get merged soon. I really need to take a break from building Tree-sitter major modes at this point. Between , , , and now , I’ve spent a lot of time staring at tree-sitter node types and indent rules. 3 It’s been fun, but I think I’ve earned a vacation from . I really wanted to do something nice for the (admittedly small) F#-on-Emacs community, and a modern major mode seemed like the most meaningful contribution I could make. I hope some of you find it useful! That’s all from me, folks! Keep hacking! Way more time than I needed to actually implement the mode.  ↩︎ Many people pointed out they thought was some package for neovim. Go figure why!  ↩︎ I’ve also been helping a bit with erlang-ts-mode recently.  ↩︎ fsharpe-mode (fsharp(evolved/enhanced)-mode) Fa Dièse (French for F sharp – because after spending time with OCaml you start thinking in French, apparently) fluoride (a play on Ionide , the popular F# IDE extension) Syntax highlighting via Tree-sitter with 4 customizable levels, supporting , , and files Indentation via Tree-sitter indent rules Imenu with fully-qualified names (e.g., ) Navigation – , , F# Interactive (REPL) integration with tree-sitter highlighting for input dotnet CLI integration – build, test, run, clean, format, restore, with watch mode support .NET API documentation lookup at point ( ) Eglot integration for FsAutoComplete Compilation error parsing for output Shift region left/right, auto-detect indent offset, prettify symbols, outline mode, and more Way more time than I needed to actually implement the mode.  ↩︎ Many people pointed out they thought was some package for neovim. Go figure why!  ↩︎ I’ve also been helping a bit with erlang-ts-mode recently.  ↩︎

0 views
(think) 3 months ago

Neocaml 0.6: Opam, Dune, and More

When I released neocaml 0.1 last month, I thought I was more or less done with the (main) features for the foreseeable future. The original scope was deliberately small — a couple of Tree-sitter-powered OCaml major modes (for and ), a REPL integration, and not much else. I was quite happy with how things turned out and figured the next steps would be mostly polish and bug fixes. Versions 0.2-0.5 brought polish and bug fixes, but fundamentally the feature set stayed the same. I was even more convinced a grand 1.0 release was just around the corner. I was wrong. Of course, OCaml files don’t exist in isolation. They live alongside Opam files that describe packages and Dune files that configure builds. And as I was poking around the Tree-sitter ecosystem, I discovered that there were already grammars for both Opam and Dune files. Given how simple both formats are (Opam is mostly key-value pairs, Dune is s-expressions), adding support for them turned out to be fairly straightforward. So here we are with neocaml 0.6, which is quite a bit bigger than I expected originally. Note: One thing worth mentioning — all the new modes are completely isolated from the core OCaml modes. They’re separate files with no hard dependency on , loaded only when you open the relevant file types. I didn’t want to force them upon anyone — for me it’s convenient to get Opam and Dune support out-of-the-box (given how ubiquitous they are in the OCaml ecosystem), but I totally get it if someone doesn’t care about this. Let me walk you through what’s new. The new activates automatically for and files. It provides: The flymake backend registers automatically when is found in your PATH, but you need to enable yourself: Flycheck users get support out of the box via Flycheck’s built-in checker — no extra configuration needed. This bridges some of the gap with Tuareg , which also bundles an Opam major mode ( ). The Tree-sitter-based approach gives us more accurate highlighting, and the flymake integration is a nice bonus on top. handles , , and files — all three use the same s-expression syntax and share a single Tree-sitter grammar. You get: This removes the need to install the separate dune package (the standalone maintained by the Dune developers) from MELPA. If you prefer to keep using it, that’s fine too — neocaml’s README has instructions for overriding the entries. Beyond editing Dune files, I wanted a simple way to run Dune commands from any neocaml buffer. is a minor mode that provides keybindings (under ) and a “Dune” menu for common operations: All commands run via Emacs’s , so you get error navigation, clickable source locations, and the full interface for free. With a prefix argument ( ), build, test, and fmt run in watch mode ( ), automatically rebuilding when files change. The command is special — it launches through , so you get the full REPL integration (send region, send definition, etc.) with your project’s libraries preloaded. This mode is completely independent from — it doesn’t care which major mode you’re using. You can enable it in OCaml buffers like this: Both the Opam and Dune Tree-sitter grammars are relatively young and will need some more work for optimal results. I’ve been filing issues and contributing patches upstream to improve them — for instance, the Dune grammar currently flattens field-value pairs in a way that makes indentation less precise than it could be, and neither grammar supports variable interpolation ( ) yet. These are very solvable problems and I expect the grammars to improve over time. At this point I think I’m (finally!) out of ideas for new functionality. This time I mean it! Neocaml now covers pretty much everything I ever wanted, especially when paired with the awesome ocaml-eglot . Down the road there might be support for OCamllex ( ) or Menhir ( ) files, but only if adding them doesn’t bring significant complexity — both are mixed languages with embedded OCaml code, which makes them fundamentally harder to support well than the simple Opam and Dune formats. I hope OCaml programmers will find the new functionality useful. If you’re using neocaml, I’d love to hear how it’s working for you — bug reports, feature requests, and general feedback are all welcome on GitHub . You can find the full list of changes in the changelog . As usual — update from MELPA , kick the tires, and let me know what you think. That’s all I have for you today! Keep hacking! Tree-sitter-based font-lock (field names, strings, operators, version constraints, filter expressions, etc.) Indentation (lists, sections, option braces) Imenu for navigating variables and sections A flymake backend that runs on the current buffer, giving you inline diagnostics for missing fields, deprecated constructs, and syntax errors Font-lock for stanza names, field names, action keywords, strings, module names, library names, operators, and brackets Indentation with 1-space offset Imenu for stanza navigation Defun navigation and support

0 views
(think) 4 months ago

Learning OCaml: String Interpolation

Most programming languages I’ve used have some form of string interpolation. Ruby has , Python has f-strings, JavaScript has template literals, even Haskell has a few popular interpolation libraries. It’s one of those small conveniences you don’t think about until it’s gone. OCaml doesn’t have built-in string interpolation. And here’s the funny thing – I didn’t even notice when I was first learning the language. Looking back at my first impressions article, I complained about the comment syntax, the semicolons in lists, the lack of list comprehensions, and a dozen other things – but never once about string interpolation. I was happily concatenating strings with and using without giving it a second thought. I only started thinking about this while working on my PPX article and going through the catalog of popular PPX libraries. That’s when I stumbled upon and thought “wait, why doesn’t OCaml have interpolation?” The short answer: OCaml has no way to generically convert a value to a string. There’s no universal method, no typeclass, no runtime reflection that would let the language figure out how to stringify an arbitrary expression inside a string literal. In Ruby, every object responds to . In Python, everything has . These languages can interpolate anything because there’s always a fallback conversion available at runtime. OCaml’s type information is erased at compile time, so the compiler would need to know at compile time which conversion function to call for each interpolated expression – and the language has no mechanism for that. 1 OCaml does have , which is actually quite nice and type-safe: The format string is statically checked by the compiler – if you pass an where expects a string, you get a compile-time error, not a runtime crash. That’s genuinely better than what most dynamically typed languages offer. But it’s not interpolation – the values aren’t inline in the string, and for complex expressions it gets unwieldy fast. There’s also plain string concatenation with : This works, but it’s ugly and error-prone for anything beyond trivial cases. ppx_string is a Jane Street PPX that adds string interpolation to OCaml at compile time. The basic usage is straightforward: For non-string types, you specify the module whose function should be used: The suffix tells the PPX to call on , and calls on . Note that , , etc. are conventions from Jane Street’s / libraries – OCaml’s uses , and so on, which won’t work with the syntax. This is another reason really only makes sense within the Jane Street ecosystem. Any module that exposes a function works here – including your own: You can also use arbitrary expressions inside the interpolation braces: Though at that point you might be better off with a binding or for readability. A few practical things worth knowing: Honestly? Probably not as much as you think. I’ve been writing OCaml for a while now without it, and it rarely bothers me. Here’s why: That said, when you do need to build a lot of human-readable strings – error messages, log output, CLI formatting – interpolation is genuinely nicer than . If you’re in the Jane Street ecosystem, there’s no reason not to use . The lack of string interpolation in OCaml is one of those things that sounds worse than it actually is. In practice, and cover the vast majority of use cases, and the code you write with them is arguably clearer about types than magical interpolation would be. It’s also a nice example of OCaml’s general philosophy: keep the language core small, provide solid primitives ( , ), and let the PPX ecosystem fill in the syntactic sugar for those who want it. The same pattern plays out with for printing, for monadic syntax, and many other conveniences. Will OCaml ever get built-in string interpolation? Maybe. There have been discussions on the forums over the years, and the language did absorb binding operators ( , ) from the PPX world. But I wouldn’t hold my breath – and honestly, I’m not sure I’d even notice if it landed. That’s all I have for you today. Keep hacking! This is the same fundamental problem that makes printing data structures harder than in dynamically typed languages.  ↩︎ You need the stanza in your dune file: String values interpolate directly, everything else needs a conversion suffix. Unlike Ruby where is called implicitly, requires you to be explicit about non-string types. This is annoying at first, but it’s consistent with OCaml’s philosophy of being explicit about types. It’s a Jane Street library. If you’re already in the Jane Street ecosystem ( , , etc.), adding is trivial. If you’re not, pulling in a Jane Street dependency just for string interpolation might feel heavy. In that case, is honestly fine. It doesn’t work with the module. If you’re building strings for pretty-printing, you’ll still want or . is for building plain strings, not format strings. Nested interpolation doesn’t work – you can’t nest inside another . Keep it simple. is good. It’s type-safe, it’s concise enough for most cases, and it’s available everywhere without extra dependencies. Most string building in OCaml happens through . If you’re writing pretty-printers (which you will be, thanks to ), you’re using , not string concatenation or interpolation. OCaml code tends to be more compute-heavy than string-heavy. Compared to, say, a Rails app or a shell script, the typical OCaml program just doesn’t build that many ad-hoc strings. This is the same fundamental problem that makes printing data structures harder than in dynamically typed languages.  ↩︎

0 views
(think) 4 months ago

Learning OCaml: PPX for Mere Mortals

When I started learning OCaml I kept running into code like this: My first reaction was “what the hell is ?” Coming from languages like Ruby and Clojure, where metaprogramming is either built into the runtime (reflection) or baked into the language itself (macros), OCaml’s approach felt alien. There’s no runtime reflection, no macro system in the Lisp sense – just this mysterious syntax that somehow generates code at compile time. That mystery is PPX (PreProcessor eXtensions), and once you understand it, a huge chunk of the OCaml ecosystem suddenly makes a lot more sense. This article is my attempt to demystify PPX for people like me – developers who want to use PPX effectively without necessarily becoming PPX authors themselves. OCaml is a statically typed language with no runtime reflection. That means you can’t do things like “iterate over all fields of a record at runtime” or “automatically serialize any type to JSON.” The type information simply isn’t available at runtime – it’s erased during compilation. One of my biggest frustrations as a newcomer was not being able to just print arbitrary data for debugging – there’s no generic or that works on any type. That frustration was probably my first real interaction with PPX. PPX solves this by generating code at compile time . When the OCaml compiler parses your source code, it builds an Abstract Syntax Tree (AST) – a tree data structure that represents the syntactic structure of your program. PPX rewriters are programs that receive this AST, transform it, and return a modified AST back to the compiler. The compiler then continues as if you had written the generated code by hand. In practical terms, this means that when you write: The PPX rewriter generates something like this behind the scenes: You get a pretty-printer for free, derived from the type definition. No boilerplate, no manual work, and it stays in sync with your type automatically. If you’ve used Rust’s or Haskell’s , the idea is very similar. The syntax is different, but the motivation is identical – generating repetitive code from type definitions. If you’re coming from Rust, you might wonder why OCaml doesn’t just have a built-in macro system like . It’s a fair question, and the answer says a lot about OCaml’s design philosophy. OCaml has always favored a small, stable language core . The compiler is famously lean and fast, and the language team is conservative about adding complexity to the specification. A full macro system baked into the compiler would be a significant undertaking – it would need to be designed, specified, maintained, and kept compatible across versions, forever. Instead, OCaml took a more minimal approach: the compiler provides just two things – extension points and attributes – as syntactic hooks in the AST. Everything else lives in the ecosystem. The actual PPX rewriters are ordinary OCaml programs that happen to transform ASTs. The ppxlib framework that ties it all together is a regular library, not part of the compiler. This has some real advantages: The trade-offs are real, though. Rust’s proc macros are more tightly integrated – you get better error messages pointing at macro-generated code, better IDE support for macro expansions, and the macro system is a documented, stable part of the language. With PPX, you’re sometimes left staring at cryptic type errors in generated code and reaching for to figure out what went wrong. That said, OCaml’s approach feels very OCaml – pragmatic, minimal, and trusting the ecosystem to build what’s needed on top of a simple foundation. And in practice, it works remarkably well. PPX wasn’t OCaml’s first metaprogramming system. Before PPX, there was Camlp4 (and its fork Camlp5 ) – a powerful but complex preprocessor that maintained its own parser, separate from the compiler’s parser. Camlp4 could extend OCaml’s syntax in arbitrary ways, which sounds great in theory but was a maintenance nightmare in practice. Every OCaml release risked breaking Camlp4, and code using Camlp4 extensions often couldn’t be processed by standard tools like editors and documentation generators. OCaml 4.02 (2014) introduced extension points and attributes directly into the language grammar – syntactic hooks specifically designed for preprocessor extensions. This was a much simpler and more maintainable approach: PPX rewriters use the compiler’s own AST, the syntax is valid OCaml (so tools can still parse your code), and the whole thing is conceptually just “AST in, AST out.” Camlp4 was officially retired in 2019. Today, the PPX ecosystem is built on ppxlib , a unified framework that provides a stable API across OCaml versions and handles all the plumbing for PPX authors. Before diving into specific libraries, let’s decode the bracket soup. PPX uses two syntactic mechanisms built into OCaml: Extension nodes are placeholders that a PPX rewriter must replace with generated code (compilation fails if no PPX handles them): Attributes attach metadata to existing code. Unlike extension nodes, the compiler silently ignores attributes that no PPX handles: The one you’ll see most often is on type declarations. The distinction between , , and is about scope – one for the innermost node, two for the enclosing declaration, three for the whole module-level. Tip: Don’t worry about memorizing all of this upfront. In practice, you’ll mostly use and occasionally or – and the specific PPX library’s documentation will tell you exactly which syntax to use. To use a PPX library in your project, you add it to the stanza in your file: That’s it. List all the PPX rewriters you need after , and Dune takes care of the rest (it even combines them into a single binary for performance). For plugins specifically, you use dotted names like . Let’s look at the PPX libraries that cover probably 90% of real-world use cases. ppx_deriving is the community’s general-purpose deriving framework. It comes with several built-in plugins: is the one you’ll reach for first – it’s essentially the answer to “how do I just print this thing?” that every OCaml newcomer asks sooner or later. The most commonly used plugins: A neat convention: if your type is named (as is idiomatic in OCaml), the generated functions drop the type name suffix – you get , , , instead of , , etc. You can also customize behavior per field with attributes: And you can derive for anonymous types inline: ppx_deriving_yojson generates JSON serialization and deserialization functions using the Yojson library: You can use or if you only need one direction. This is incredibly useful in practice – writing JSON serializers by hand for complex types is tedious and error-prone. If you’re using Jane Street’s Core library, you’ll encounter S-expression serialization everywhere. ( Tip: Jane Street bundles most of their PPXs into a single ppx_jane package, so you can add just to your instead of listing each one individually.) ppx_sexp_conv generates converters between OCaml types and S-expressions: The attributes here are quite handy – provides a default value during deserialization, and means the field is represented as a present/absent atom rather than . Two more Jane Street PPXs that you’ll see a lot in Core-based codebases. ppx_fields_conv generates first-class accessors and iterators for record fields: ppx_variants_conv does something similar for variant types – generating constructors as functions, fold/iter over all variants, and more. These Jane Street PPXs let you write tests directly in your source files: ppx_expect is particularly nice – it captures printed output and compares it against expected output: If the output doesn’t match, the test fails and you can run to automatically update the expected output in your source file. It’s a very productive workflow for testing functions that produce output. ppx_let provides syntactic sugar for working with monads and other “container” types: How does know which to call? It looks for a module in scope that provides the underlying and functions. In practice, you’ll typically open a module that defines before using : Note: Since OCaml 4.08, the language has built-in binding operators ( , , , ) that cover the basic use cases of without needing a preprocessor. If you’re not using Jane Street’s ecosystem, binding operators are probably the simpler choice. still offers extra features like , , and optimized though. ppx_blob is beautifully simple – it embeds a file’s contents as a string at compile time: No more worrying about file paths at runtime or packaging data files with your binary. The file contents become part of your compiled program. One thing that’s always bugged me about OCaml is the lack of string interpolation. ppx_string fills that gap: The suffix tells the PPX to convert the value using . You can use any module that provides a function. Most OCaml developers will never need to write a PPX, but understanding the basics helps demystify the whole system. Let’s build a very simple one. Say we want an extension that converts a string literal to uppercase at compile time. Here’s the complete implementation using ppxlib : The dune file: The key pieces are: For more complex PPXs (especially derivers), you’ll also want to use Metaquot ( ), which lets you write AST-constructing code using actual OCaml syntax instead of manual AST builder calls: The ppxlib documentation has excellent tutorials if you want to go deeper. One practical tip: when something goes wrong with PPX-generated code and you’re staring at a confusing type error, you can inspect what the PPX actually generated: Seeing the expanded code often makes the error immediately obvious. Most of the introductory PPX content out there was written around 2018-2019, so it’s worth noting how things have evolved since then. The big story has been ppxlib’s consolidation of the ecosystem . Back in 2019, some PPX rewriters still used the older (OMP) library, creating fragmentation. By 2021, nearly all PPXs had migrated to ppxlib , effectively ending the split. Today ppxlib is the way to write PPX rewriters – there’s no real alternative to consider. The transition hasn’t always been smooth, though. In 2025, ppxlib 0.36.0 bumped its internal AST to match OCaml 5.2, which changed how functions are represented in the parse tree. This broke many downstream PPXs and temporarily split the opam universe between packages that worked with the new version and those that didn’t. The community worked through it with proactive patching, but it highlighted an ongoing tension in the PPX world: ppxlib shields you from most compiler changes, but major AST overhauls still ripple through the ecosystem. On the API side, ppxlib is gradually deprecating its copy of in favor of , with plans to remove entirely in a future 1.0.0 release. If you’re writing a new PPX today, use exclusively. Meanwhile, OCaml 4.08’s built-in binding operators ( , , etc.) have reduced the need for in projects that don’t use Jane Street’s ecosystem. It’s a nice example of the language absorbing a pattern that PPX pioneered. Perhaps one day we’ll see more of this (e.g. native string interpolation). This article covers a lot of ground, but the PPX topic is pretty deep and complex, so depending on how far you want to go you might want to read more on it. Here are some of the best resources I’ve found on PPX: I was amused to see whitequark’s name pop up while I was doing research for this article – we collaborated quite a bit back in the day on her Ruby parser project, which was instrumental to RuboCop . Seems you can find (former) Rubyists in pretty much every language community. This article turned out to be a beast! I’ve wanted to write something on the subject for quite a while now, but I’ve kept postponing it because I was too lazy to do all the necessary research. I’ll feel quite relieved to put it behind me! PPX might look intimidating at first – all those brackets and symbols can feel like line noise. But the core idea is simple: PPX generates boilerplate code from your type definitions at compile time. You annotate your types with what you want ( , , , , etc.), and the PPX rewriter produces the code you’d otherwise have to write by hand. For day-to-day OCaml programming, you really only need to know: The “writing your own PPX” part is there for when you need it, but honestly most OCaml developers get by just fine using the existing ecosystem. That’s all I have for you today. Keep hacking! The ecosystem can evolve independently. ppxlib can ship new features, fix bugs, and improve APIs without waiting for a compiler release. Compare this to Rust, where changes to the proc macro system require the full RFC process and a compiler update. Tooling stays simple. Because and are valid OCaml syntax, every tool – editors, formatters, documentation generators – can parse PPX-annotated code without knowing anything about the specific PPX. The code is always syntactically valid OCaml, even before preprocessing. The compiler stays lean. No macro expander, no hygiene system, no special compilation phases – just a hook that says “here, transform this AST before I type-check it.” – registers an extension with a name, the context where it can appear (expressions, patterns, types, etc.), the expected payload pattern, and an expansion function. – a pattern-matching DSL for destructuring AST nodes. Here matches a string literal and captures its value. – helpers for constructing AST nodes. builds a string literal expression. – registers the rule with ppxlib’s driver. Preprocessors and PPXs – the official OCaml documentation on metaprogramming. A solid reference, though it assumes some comfort with the compiler internals. An Introduction to OCaml PPX Ecosystem – Nathan Rebours’ 2019 deep dive for Tarides. This is the most thorough tutorial on writing PPX rewriters I’ve seen. Some API details have changed since 2019 (notably the → shift), but the concepts and approach are still excellent. ppxlib Quick Introduction – ppxlib’s own getting-started guide. The best place to begin if you want to write your own PPX. A Guide to PreProcessor eXtensions – OCamlverse’s reference page with a comprehensive list of available PPX libraries. A Guide to Extension Points in OCaml – Whitequark’s original 2014 guide that introduced many developers to PPX. Historically interesting as a snapshot of the early PPX days. on type declarations to generate useful functions How to add PPX libraries to your dune file with Which PPX libraries exist for common tasks (serialization, testing, pretty-printing)

0 views
(think) 5 months ago

Neocaml 0.1: Ready for Action

neocaml 0.1 is finally out! Almost a year after I announced the project , I’m happy to report that it has matured to the point where I feel comfortable calling it ready for action. Even better - recently landed in MELPA , which means installing it is now as easy as: That’s quite the journey from “a fun experimental project” to a proper Emacs package! You might be wondering what’s wrong with the existing options. The short answer - nothing is wrong per se, but offers a different set of trade-offs: Of course, is the youngest of the bunch and it doesn’t yet match Tuareg’s feature completeness. But for many OCaml workflows it’s already more than sufficient, especially when combined with LSP support. I’ve started the project mostly because I thought that the existing Emacs tooling for OCaml was somewhat behind the times - e.g. both and have features that are no longer needed in the era of . Let me now walk you through the highlights of version 0.1. The current feature-set is relatively modest, but all the essential functionality one would expect from an Emacs major mode is there. leverages TreeSitter for syntax highlighting, which is both more accurate and more performant than the traditional regex-based approaches used by and . The font-locking supports 4 customizable intensity levels (controlled via , default 3), so you can pick the amount of color that suits your taste. Both (source) and (interface) files get their own major modes with dedicated highlighting rules. Indentation has always been tricky for OCaml modes, and I won’t pretend it’s perfect yet, but ’s TreeSitter-based indentation engine is already quite usable. It also supports cycle-indent functionality, so hitting repeatedly will cycle through plausible indentation levels - a nice quality-of-life feature when the indentation rules can’t fully determine the “right” indent. If you prefer, you can still delegate indentation to external tools like or even Tuareg’s indentation functions. Still, I think most people will be quite satisfied with the built-in indentation logic. provides proper structural navigation commands ( , , ) powered by TreeSitter, plus integration definitions in a buffer has never been easier. The older modes provide very similar functionality as well, of course, but the use of TreeSitter in makes such commands more reliable and robust. No OCaml mode would be complete without REPL (toplevel) integration. provides all the essentials: The default REPL is , but you can easily switch to via . I’m still on the fence on whether I want to invest time into making the REPL-integration more powerful or keep it as simple as possible. Right now it’s definitely not a big priority for me, but I want to match what the other older OCaml modes offered in that regard. works great with Eglot and , automatically setting the appropriate language IDs for both and files. Pair with ocaml-eglot and you get a pretty solid OCaml development experience. The creation of LSP really simplified the lives of a major mode authors like me, as now many of the features that were historically major mode specific are provided by LSP clients out-of-the-box. That’s also another reason why you probably want to leaner major mode like . But, wait, there’s more! There’s still plenty of work to do: If you’re following me, you probably know that I’m passionate about both Emacs and OCaml. I hope that will be my way to contribute to the awesome OCaml community. I’m not sure how quickly things will move, but I’m committed to making the best OCaml editing experience on Emacs. Time will tell how far I’ll get! If you’re an OCaml programmer using Emacs, I’d love for you to take for a spin. Install it from MELPA, kick the tires, and let me know what you think. Bug reports, feature requests, and pull requests are all most welcome on GitHub ! That’s all from me, folks! Keep hacking! is ancient and barely maintained. It lacks many features that modern Emacs users expect and it probably should have been deprecated a long time ago. is very powerful, but also very complex. It carries a lot of legacy code and its regex-based font-locking and custom indentation engine show their age. It’s a beast - in both the good and the bad sense of the word. aims to be a modern, lean alternative that fully embraces TreeSitter. The codebase is small, well-documented, and easy to hack on. If you’re running Emacs 29+ (and especially Emacs 30), TreeSitter is the future and is built entirely around it. - Start or switch to the OCaml REPL - Send the current definition - Send the selected region - Send the entire buffer - Send a phrase (code until ) to quickly switch between and files Prettify-symbols support for common OCaml operators Automatic installation of the required TreeSitter grammars via Compatibility with Merlin for those who prefer it over LSP Support for additional OCaml file types (e.g. ) Improvements to structured navigation using newer Emacs TreeSitter APIs Improvements to the test suite Addressing feedback from real-world OCaml users Actually writing some fun OCaml code with

0 views
Luke Hsiao 5 months ago

The outsized impact of cultural idiosyncrasies

In the context of companies, cultural idiosyncrasies are fascinating to me. I’m of the opinion that culture (including mission, principles, and values) is one of the biggest factors that differentiate companies from one another. Sure, the product or service is often the largest differentiator, but company cultures—and in particular, their idiosyncrasies—have an outsized impact on public image, internal perception, and job satisfaction. I believe it’s because these idiosyncrasies are evidence of what a company does , and not just what it says it does. They reflect principles and values in uniquely powerful ways to both potential customers and employees. Let me draw some examples from my own lived experience, and I’m sure I’m missing some compelling examples further back (Sun Microsystems, Xerox PARC). Also, and importantly, I’m not saying any of these idiosyncrasies are “right”, but I am saying they all have an outsized impact on my perception of the companies. One of the earliest examples from my own memory is Google. Not only did I experience how Google Search took over the market with its minimalism and effectiveness, but I also grew up hearing all sorts of news about how Google was “the best place to work”. In those articles, did they talk about search? Nope. They talked about cultural idiosyncrasies: free meals from on-site cooks, nap pods, 20% projects, slides in the lobbies, micro-kitchens, laundry services! Oxide is a prime example. They frequently feature in online discussion, but in my perception, the majority of this discussion isn’t about their product; it’s about their culture, often with highly positive sentiment. They find this so core to their company that they dedicated a podcast episode to it . Some examples are demo Fridays, morning water-cooler, no-meet Wednesdays , recorded meetings, dog-pile debugging, RFDs (requests for discussion), no performance reviews, an overwhelmingly writing-focused hiring process , and uniform and transparent pay , to name a few. GitLab may not have the popularity of GitHub, but they have made a strong impression on me for at least two of their idiosyncrasies. The first was particularly prominent during the COVID-19 pandemic: a global, all-remote workforce . They are one of the largest all-remote companies with over 1,500 members in 65+ countries. They are also known for their transparent, handbook-first approach . They publish their handbook for all to see. This is not brief, either. It comprehensively covers everything: engineering, finance, sales, legal, and more. This is an incredible show of transparency and goodwill in sharing hard-earned lessons publicly. Not to be outdone, the 37signals folks also publish their handbook . In their case, they have also published highly profitable books about their idiosyncrasies and culture, such as Shape Up (an engineering methodology that many companies are now adopting), REWORK (unconventional business advice), and REMOTE (on remote work). They also have idiosyncrasies like Omarchy , an opinionated distribution of Arch Linux. They liked it so much they ditched MacBooks as the standard-issue developer laptop and switched to Framework laptops. Tonari is a small Japanese tech startup making some of the most interesting audio/video portals available. Despite being very small, they contribute significant pieces of their stack to open source, such as innernet , a private network system using WireGuard under the hood. They also value a heterogeneous mix of developer machines and were early on the Rust train (note how each of the four engineers mentioned in this blog runs a different operating system: macOS, Arch, Ubuntu, and Pop!_OS). Palantir breaks the mold a bit in this list, because most of the public discussion around it is controversial and focused on the ethics of its business. That said, they are known for their idiosyncratic chaos culture , where “everything is up for debate”—even a random engineer confronting the CEO at all-hands—and where they do almost anything to move fast—like chartering a private jet to Palo Alto to get the engineering team together during the COVID-19 pandemic. People I know who have worked there tell me this is an accurate representation. Famously, it’s the OCaml shop people know about. Compensation so high, even for interns, that it’s an idiosyncrasy. And, most notably to me, they have one of the most interesting tech blogs around. For example, their code review process has such a good reputation that other tech bloggers carve out exceptions for it when talking about code review problems. They care so much about rigor in tests that they not only leverage bleeding-edge strategies like deterministic simulation testing (DST) but also led the funding round of Antithesis, the pioneers in DST . This is highly idiosyncratic. Even for massive companies like Amazon, some cultural idiosyncrasies still have an outsized impact. For example, I suspect most people have heard of Amazon’s 6-page memo approach to meetings. In this approach, attendees don’t read anything beforehand; instead of using PowerPoint, executives sit around a table and read six-page memos in silence before discussion commences. Likewise, Amazon is well known for its two-pizza teams , where the idea is that a team should be no larger than two pizzas can feed. That way it stays small, cohesive, and efficient. Zappos is well known for its unique pay-to-quit approach to making sure the team is invested. It provides an attractive off-ramp for those who are less committed to or interested in the company after their training period, instead of staying despite lacking passion for the work. Patagonia is well known for its Let My People Go Surfing policy, which prioritizes work-life balance, purpose, and employee autonomy. The policy encourages staff to embrace flexibility and spontaneity in balancing work and play. If the waves are good or the powder fresh, staff are trusted to step away, surf, or enjoy nature, returning to work with renewed purpose. Valve is the famously “flat” company, or “flatland”, as their public handbook calls it . They “don’t have any management, and nobody reports to anybody.” They playfully poke at Google’s 20% time by saying that at Valve, it’s 100%. Of course, this comes with its challenges, but this idiosyncrasy has become an integral part of my perception of Valve. Sample size of 1, but I have a friend who really appreciates Vannevar’s $250/month stipend for mental/physical health and $300/month stipend to pay for house cleaners. These dedicated benefits are particularly on point for a mostly remote team. As a company they heavily hire forward-deployed engineers (FDEs) and empower them with very high autonomy. I’m sure this list could be ten times longer with some more thought. But perhaps that supports the point. All of these examples came almost immediately off the top of my head! If you’re running a company, I think it’s important to be intentional about your culture. Both in the mission, principles, and values you profess and in the idiosyncrasies that uniquely evolve because of them. People will talk about your cultural idiosyncrasies to the benefit or detriment of your company. Your employees will reach for your idiosyncrasies when they talk about what is good or bad about working for your company. Your customers will point to your idiosyncrasies as evidence for or against value alignment. Your future employees will compare and contrast your idiosyncrasies with their experiences as they consider their next career move. And random people like me will evidently spend (too much?) time thinking about your idiosyncrasies and trying to theory-craft the perfect set of cultural idiosyncrasies for their hypothetical future company.

0 views

Type safe interpreters

It is well known that most software has bugs [Citation needed] . It is also well known that interpreters are software 1 . One issue that one might run into in an interpreter is type safety : It may very well be possible for your typed language's interpreter to nevertheless end up in a state where it is asked to evaluate . What can we do about that? GADTs are a technique for increasing type safety in a program, by "indexing" a constructor by a type. One of the canonical examples for this is an -like. Imagine we wanted a type-safe way to ensure that in some cases, we could always extract the value inside. One way of doing this is to add an extra type parameter, and use that to mark whether something is inside. We will use OCaml for this demonstration. Note that we have used a different syntax than usual. Normally, we put a type parameter in the constructor, like , and then use that throughout ( ). However, as we are changing that type in the "return" of the constructor, we need a syntax that allows us to change that. marks a type parameter we "don't need to name" (as we always specify it), and in this case, we always specify both. Then, we add our elements before a , mimicking a function. The general form of this in OCaml is . Now, if we have something of type , we know it contains a value! The only other way a can be formed is via , but that gives it type , so it'd be type-incorrect. This means we can write a function like and have it be total; there is no missing case, because the missing case is a type error. Note that when writing a function like , we need to use this interesting piece of syntax: This is as otherwise OCaml eagerly thinks "Ah, must be !" (or equivalent for ) when seeing the case in the pattern match. The designator tells it to keep as general as possible, instead of refining it. How does this allow us to get a more type-safe interpreter, though? Well, let's start by introducing our expression type: We've added space for a type parameter. What should integers and booleans look like? Maybe they should be indexed by their respective "meta" types. Now, we want to add a case for and . Naturally, we shouldn't be able to add two booleans, so let's restrict it to only taking in s. Similar for negate. We've already added type safety! It's impossible to write , because expects a , but is a . Let's keep going: (or ) should take two integers and return a boolean, and should take a boolean as its first argument, but anything for its second and third. Hence note that we can still use generic parameters - we just need to think about when it makes sense to do so, to get the type safety we want. Now for functions. What should a function look like here? One way of implementing them is with a "Higher-order abstract syntax"(HOAS) 3 approach, where we use the functions of the host language to make functions in the interpreted language. In that case, we should take a function as the argument to our constructor, but when what is our result type? That function type! We'll need to know it later to safely write , so we "store" it in the type now. We also want the function to take and return an , so we can do constructions like . Note that the function is not an at first, as otherwise we would have no way to construct it in the first place. (Remember, this is how we make functions.) Then . It should take in a function, and a value, and then return the function applied to the value. This hence translates as: As a finale, we'll add pairs and projections. I won't elaborate on these, hoping that it's becoming obvious how they work. Now let's look at some examples and their types. We have that a construction like is not just an error, but a type error ! This means it's caught at compile time! From here, the evaluator itself is very simple. We just write the interpretation of our language, as usual: The and cases are interesting. In the former, we need to return a function of type . We can start by introducing a function , but we have , so we just apply it. In the latter, we need to to get a function from , which is of type . We can then pass it , which is of type to get a , which we must then evaluate again to get a . Now we can run the above examples. Remember, all of the above is completely type safe! We have succesfully constructed an interpreter that can never type error. In short, it can be hard. One way is to have a "base" language that's parsed, and then have your typechecker produce a representation like the above that you then know is type correct. However, this does prevent typechecker bugs from sneaking through, which was our entire goal! To some extent, this is kicking the bucket down the road - you are now relying that your "host language"'s typechecker is correct. However, this is a numbers game; if you're writing in OCaml or Haskell or similar, the chances of that are very very high unless you're using extremely weird features. The astute may have noted that this is essentially a version of denotional semantics. We're interpreting into the type-safe domain of the host language to ensure that the types always line up (as they must, for the interpretation to succeed, and the typechecker of the host language ensures that). No, I won't do the same joke twice. Most of the time. ↩ I am not going into what "ADT" stands for, as it's a whole nother debate. ↩ Higher-order abstract syntax. A bit too in-depth to explore in full right now, but worth exploring. ↩ No, I won't do the same joke twice. Most of the time. ↩ I am not going into what "ADT" stands for, as it's a whole nother debate. ↩ Higher-order abstract syntax. A bit too in-depth to explore in full right now, but worth exploring. ↩

0 views
(think) 10 months ago

Why I Chose Ruby over Python

This year I spent a bit of time playing with Python, after having mostly ignored it since 2005 when was learning it originally. I did like Python back then, but a few years afterwards I discovered Ruby and quickly focused my entire attention on it. There were many (mostly small) reasons why I leaned towards Ruby back then and playing with Python now made me remember a few of them. I thought it might be interesting to write a bit about those, so here we go. Disclaimer: This is not a rant and I know that: So, treat this as an amusing personal account and nothing more than that. Probably the thing about Python that bothered me the most what stuff that would normally be methods are global functions (that often call some object methods internally). I’m referring to the likes of: You can find the full list here . I’m guessing the reason for this (as usual) is historical, but I much prefer the way Ruby does things. E.g. instead of or vs . is a pretty weird beast as it can: Why would someone want a keyword for removing items from lists and dictionaries instead of some method is beyond me. Ruby’s arrays have methods like: In Ruby almost everything is an expression (meaning that evaluating it would result in a value). In Python a lot of things are consider “statements” - something executed for their side effects only. If you haven’t used languages like Ruby or Lisp this might sound a bit strange, but if we go back to the previous section about , we can observe that: That’s something that I really value and I consider it one of the bigger practical advantages of Ruby over Python. At first I thought the semantic indentation used by Python is super cool, as it reduces a bit the typing one needs to do: In hindsight, however, I quickly realized that it also: One more thing - 4 spaces by default seems a tad too much to me, although that’s obviously debatable. P.S. I feel obliged to admit I’m not a big fan of either and would have preferred instead, but it is how it is. I’m not a fan of Python’s type as for me it’s a pretty weird duck: I get how things ended up the way they are, but for me it’s not OK to be able to write code like . I’m also not a fan of treating empty collection literals as , although I definitely have less issues with this than with 0 and 1. To compare this with Ruby: This definitely resonates better with me. Side note: Lately I’ve been playing a lot with languages like OCaml, F# and Rust, that’s why to me it now feels extra strange to have a boolean type that works like an integer type. I really like the range literals in Ruby: Python has the function that kind of gets the job done, but for whatever reason it doesn’t have the option to mark something as inclusive range. Definitely not a big deal, but one of the many small touches of Ruby’s syntax that I came to appreciate over time. In Python one has to pass to instance methods explicitly, which always seemed to me like an excessive level of verbosity. Also, that’s quite uncommon in other object-oriented programming languages. Many special methods have names surrounded with , which I find both odd and not very easy to type. I get why this was chosen (to avoid naming conflicts), but I don’t like it regardless. I really like that in Ruby the return value of a method is the value of the last expression that got evaluated in the method. There’s a expression in Ruby, but it’s rarely needed in practice. In Python, by comparison, you always have to use , otherwise your method will return . Not a big deal in general, but as I spend a lot of time with Ruby and various functional programming languages, it’s definitely something that bothers me. Admittedly that’s a very small one, but I would have preferred if anonymous functions were created with a keyword like or instead of . In Ruby historically they were created with as well, but afterwards the shorthand was introduced as well. There’s nothing wrong with the Python syntax per se, but I think that in general for lambdas it’s better to have a more compact syntax. Ruby predicates typically have names ending in - e.g. , , . This makes them really easy to spot while reading some code. Python sticks to the more common convention of prefixing such methods with , , etc and that’s fine. One thing that bothers me a bit is that often there’s not spaces between the prefix and the rest of the name (e.g. ), which doesn’t read great in my opinion. More importantly, in Ruby and Python it’s common to have destructive and non-destructive versions of some methods. E.g. - vs in Ruby, and vs in Python. I don’t know about you, but to me it seems that: I’m guessing the reasons here are also historical. Multi-line text literals are common in many languages, but I’m not super fond of: Who thought that typing those would be easy? It’s not that HEREDOCS in Ruby are great either, but I guess they are at least more common in various programming languages. Ruby has and . That’s it. Everyone uses them. Life is simple. Things are a lot more complicated in the realm of Python where several different tools have been in fashion over the years: Now it seems that might replace them all. Until something replaces I guess… And that’s a wrap. I’m guessing at this point most Rubyists reading this would probably agree with my perspective (shocking, right?) and most Pythonistas won’t. And that’s fine. I’m not trying to convince anyone that Ruby’s a better language than Python, I’m just sharing the story of how I ended up in team Ruby almost 20 years ago. Back in the day I felt that Ruby’s syntax was more elegant and more consistent than Python’s, and today my sentiment is more or less the same. Don’t get me wrong, though - I like Python overall and enjoy using it occasionally. It just doesn’t make me as happy as Ruby does. I’ve long written about my frustrations with Ruby, so it feels pretty good to write for once about the aspects of Ruby that I really enjoy. Keep hacking! P.S. After writing this I realized I had already written a similar article 14 years ago, but I had totally forgotten about it! Oh, well… the things I prefer in Ruby over Python are super subjective for every thing that Ruby does “better” there’s something else that Python does better delete variables dictionary items (to remove some values) (to remove some index) There’s no obvious way to determine if actually removed something In Ruby, however, most deletions result in informative results makes things harder for tooling, as it can’t really rearrange indented code sections in editors you can’t have much in terms of auto-indenting as you type (as the editor can’t know when a block finishes without you outdenting explicitly) was added only in Python 2.3 it inherits from and are essentially 1 and 0 has nothing to do with numbers there the only things are considered logically false are and (inclusive range) (exclusive range) Ruby encourages you to favor the non-destructive versions of the methods, unlike Python Ruby’s more consistent than Python

0 views
(think) 12 months ago

Learning OCaml: Having Fun with the Fun Module

When I started to play with OCaml I was kind of surprised that there was no (identity) function that was available out-of-box (in module, that’s auto-opened). A quick search lead me to the Fun module, which is part of the standard library and is nested under . It was introduced in OCaml 4.08, alongside other modules such as , and . 1 The module provides a few basic combinators for working with functions. Let’s go over them briefly: The identity function: returns its single argument unchanged. Returns a function that always returns the first argument, ignoring its second argument. Composes two functions, applying the second function to the result of the first. Haskell and F# have special syntax for function composition, but that’s not the case in OCaml. (although you can easily map this to some operator if you wish to do so) Also, introduced a bit later than the other functions in the module - namely in OCaml 5.2. Reverses the order of arguments to a two-argument function. Negates a boolean-returning function, returning the opposite boolean value. Useful when you want to provide a pair of inverse predicates (e.g. and ) I believe that those functions are pretty self-explanatory, but still below we’ll go over a few examples of using them: Admittedly the examples are not great, but I hope they managed to convey how to use the various combinators. Those are definitely not the type of functions that you would use every day, but they can be useful in certain situations. Obviously I needed at some point to discover the module in the first place, and all of the functions there can be considered “classic” combinators in functional programming. In practice most often I need and , and infrequently and . Right now I’m struggling to come up with good use-cases for , but I’m sure those exist. Perhaps you’ll share some examples in the comments? How often do you use the various combinators? Which ones do you find most useful? I find myself wondering if such fundamental functions shouldn’t have been part of module directly, but overall I really like the modular standard library approach that OCaml’s team has been working towards in the past several years. 2 The important thing in the end of the day is to know that these functions exist and you can make use of them. Writing this short article will definitely help me to remember this. That’s all I have for you today. Keep hacking! It was part of some broader efforts to slim down and move in the direction of a more modular standard library.  ↩ And obviously you can open the module if you wish to at whatever level you desire.  ↩ The identity function: returns its single argument unchanged. Returns a function that always returns the first argument, ignoring its second argument. Composes two functions, applying the second function to the result of the first. Haskell and F# have special syntax for function composition, but that’s not the case in OCaml. (although you can easily map this to some operator if you wish to do so) Also, introduced a bit later than the other functions in the module - namely in OCaml 5.2. Reverses the order of arguments to a two-argument function. Negates a boolean-returning function, returning the opposite boolean value. Useful when you want to provide a pair of inverse predicates (e.g. and ) It was part of some broader efforts to slim down and move in the direction of a more modular standard library.  ↩ And obviously you can open the module if you wish to at whatever level you desire.  ↩

0 views
(think) 12 months ago

Learning OCaml: Numerical Type Conversions

Today I’m going to cover a very basic topic - conversions between OCaml’s primary numeric types and . I guess most of you are wondering if such a basic topic deserves a special treatment, but if you read on I promise that it will be worth it. So, let’s start with the basics that probably everyone knows: Both functions live in module, which is opened by default in OCaml. Here it gets a bit more interesting. For whatever reasons there’s also a function, that’s a synonym to . There’s no function, however. Go figure why… Here’s a bit of trivia for you - does truncation to produce an integer. And there’s also a function that’s another alias for . Again, for whatever reasons it seems there are no functions that allow you to produce an integer by rounding up or down. (although such functions exist for floats - e.g. , and ) More interestingly, OCaml 4.08 introduced the modules and that bring together common functions for operating on integers and floats. 1 And there are plenty of type conversion functions in those modules as well: The introduction of the and modules was part of (ongoing) effort to make OCaml’s library more modular and more useful. I think that’s great and I hope you’ll agree that most of the time it’s a better idea to use the new modules instead of reaching to the “historical” functions in the module. Sadly, most OCaml tutorials out there make no mention of the new modules, so I’m hoping that my article (and tools like ChatGPT) will steer more people in the right direction. If you’re familiar with Jane Street’s , you’ll probably notice that it also employs similar structure when it comes to integer and float functionality. Which type conversion functions do you prefer? Why? That’s all I have for you today. Keep hacking! Technically speaking, existed before 4.08, but it was extended then. 4.08 also introduced the modules , , and . Good stuff!  ↩ you can convert integers to floats with you can convert floats to integers with Technically speaking, existed before 4.08, but it was extended then. 4.08 also introduced the modules , , and . Good stuff!  ↩

0 views

Crafting a dependent typechecker, part 1

This series is intended to give an interested layperson some idea of what goes on behind-the-scenes in a dependent typechecker, and how they might implement one of their own. I personally hold the belief that dependent languages (or similar) will eventually be the future of programming, so knowing this information seems worthwhile to me. One may also simply be interested for the sake of it, or may be interested in implementing their own dependent language. This series does assume a small base amount of knowledge around dependent programming languages. There are many tutorials out there far better than I could provide, so I will refer the reader to the wider internet if they are curious. (Return here afterwards, though!). Additionally, the code snippets will be in OCaml, although a Haskell or Rust version may be made available at a later date. The technique we will implement in this series can most succinctly be described as "Bidirectional typechecking, utilizing Normalization by Evaluation". I do not expect the reader to understand that sentence for at least a little while ;). All code snippets will be collated at the end of each page, so scroll all the way down if you're only interested in that. The following are resources I highly recommend for exploring this topic on your own. If you have implemented a typechecker before, you may have come to the realization that deciding when two things are the same is a very large component of typechecking. Whether it be simply checking that two arguments to are both integers, or larger problems with polymorphism, the arguable core of typechecking is deciding equality. In a dependent language, our types can (and often do) contain expressions that must be evaluated. Therefore, in order to explore how we check equality in the presence of evaluation, we will first start by considering how we do this for a very simple language - integer arithmetic. Take two expressions: How might we decide whether these are equal? The obvious solution is to evaluate them. Indeed, (sparing the reader the derivation), we can see they both evaluate to , and so they are equal. Let us now introduce a small extension: Variables with a known value, using "let". We will refer to the variables themselves as "bound variables", as they are bound by the "let". In this case, they are additionally bound to a value. Terms containing only bound variables are referred to "closed" terms - this alludes to, for example, closed and open systems in the real world. Getting back to evaluation, we could simply substitute the computed value: And then evaluate. (In this case, our result is 117.) However, what if the variable occurs multiple times? The following is a little bad, but not awful: But what if was the following? Duplicating that would clearly not be ideal! Perhaps we could compute in advance, and then substitute that result in. This is much better: (In fact, there is yet another optimization to do: What if we don't need to compute it at all, say because it's not used? We will explore this possibility later.) A simple OCaml interpretation of the terms of this language may be the following. (sidetrack: A "term" is some element of a language. Above, we have the language of (restricted) integer arithmetic, and our terms are expressions of said. Hence, we call our OCaml interpretation of this language .) We then must make an environment that associates, or "binds", our names to terms. is often called a "binder" because it functions as the source of this binding. We will make our context a list of pairs , and will reimpliement the lookup function for example's sake, although a suitable function is available as . Note: In this series, we will not be considering the possibility that a name is used without being previously defined in some way. This is an error, and can be handled via normals means, e.g. erroring. We now note something curious: Our lookup function need not return a fully computed value! It could return , which we would have to further evaluate before we could do anything with. It would be possible of course to manually insert invariants ensuring that this is not the case, but we could still accidentally violate them. Here we hit one of the most important distinctions in our exploration of dependent typechecking. A "term", as previously seen, is any element of a chosen language. Above, this was (some subset of) integer arithmetic. We could also consider the language of OCaml programs, of which the above examples would be terms. However, two terms are not always easy to compare. We may need to evaluate one, or both, and we don't know whether that will be needed until we start comparing them. Instead, what if we defined another type that was guaranteed to be reduced to some suitable level? For this simple case, we could restrict this to contain only fully evaluated terms (i.e., integers). We prefix these constructors with to ensure they're distinct. Then, we could modify our environment and lookup function to only deal with values: and now we know that will always return something we can handle immediately, with no further processing. In the general case, we call this "suitably-reduced" constraint a "normal form". We can say that contains only elements of in this chosen normal form. Now we can finish our little evaluator. We do a slight hack to perform arithmetic inside values; if our value type contained more than just the constructor , we would have to consider how to handle that case. This is something we will cover soon. In the case, we compute the definition first, then extend our environment with the new name and value before computing the rest of the expression. We then, by returning a , have a guarantee that whatever comes out of is as simple as we want it to be. Comparing for equality is then very simple: We use to evaluate our terms into values, then because our values are in a nice normal form, we can compare them easily guaranteed. So far we have covered: In the next part, we will explore how to take these concepts and apply them to something much closer to a "real" programming language, with expressions and application, which will require the introduction of a "closure". From there, we can explore how to deal with open terms, containing free variables, and onwards to dependent types. András Kovacs' "Elaboration Zoo", found here . It was a primary source of inspiration for this series. "A tutorial implementation of a dependently typed lambda calculus", by Andres Löh, Conor McBride, and Wouter Swierstra; found here . "Checking Dependent Types with Normalization by Evaluation: A Tutorial", by David Christiansen, found here , for the more lisp-minded. Languages and terms. Simple evaluation strategies for let bound variables & closed terms. Values and normal forms.

0 views
Max Bernstein 1 years ago

What I talk about when I talk about IRs

I have a lot of thoughts about the design of compiler intermediate representations (IRs). In this post I’m going to try and communicate some of those ideas and why I think they are important. The overarching idea is being able to make decisions with only local information. That comes in a couple of different flavors. We’ll assume that we’re compiling a method at a time, instead of a something more trace-like (tracing, tracelets, basic block versioning, etc). A function will normally have some control-flow: , , , any amount of jumping around within a function. Let’s look at an example function in a language with advanced control-flow constructs: Most compilers will deconstruct this , with its many nested expressions, into simple comparisons and jumps. In order to resolve jump targets in your compiler, you may have some notion of labels (in this case, words ending with a colon): This looks kind of like a pseudo-assembly language. It has its high-level language features decomposed into many smaller instructions. It also has implicit fallthrough between labeled sections (for example, into ). I mention these things because they, like the rest of the ideas in this post, are points in an IR design space. Representing code this way is an explicit choice, not a given. For example, one could make the jumps explicit by adding a at the end of . As soon as we add that instruction, the code becomes position-independent: as long as we start with , the chunks of code between labels could be ordered any which way: they are addressed by name and have no implicit ordering requirements. This may seem arbitrary, but it gives the optimizer more flexibility. If some optimization rule decides, for example, that a branch to may rarely be taken, it can freely re-order it toward the end of the function (or even on a different page!) so that more hot code can be on a single cache line. Explicit jumps and labels turn the code from a strictly linear assembly into a control-flow graph (CFG). Each sequence of code without internal control-flow is a called basic block and is a vertex in this graph. The directed edges represent jumps between blocks. See for example this crude GraphViz representation: We’re actually kind of looking at extended basic blocks (EBBs), which allow for multiple control exits per block but only one control entry. A strict basic block representation of the above code would look, in text form, something like this: Notice how each block has exactly one terminator (control-flow instruction), with (in this case) 0 or 2 targets. Opinions differ about the merits and issues of extended vs normal basic blocks. Most compilers I see use normal basic blocks. In either case, bringing the IR into a graph form gives us an advantage: thanks to Cousot and Cousot, our favorite power couple, we know how to do abstract interpretation on graphs and we can use this to build an advanced optimizer. See, for example, my intro to abstract interpretation post . Some IRs are stack based. For concatenative languages or some newer JIT compilers, IRs are formatted in such a way that each opcode reads its operands from a stack and writes its outputs to a stack. This is reminiscent of a point-free coding style in languages such as Haskell or OCaml. In this style, there is an implicit shared state: the stack. Dataflow is explicit (pushes and pops) and instructions can only be rearranged if the stack structure is preserved. This requires some non-local reasoning: to move an instruction, one must also rearrange the stack. By contrast, in a register-based IR, things are more explicit. Instructions take named inputs ( , , etc) and produce named outputs. Instructions can be slightly more easily moved around (modulo effects) as long as inputs remain defined. Local variables do not exist. The stack does not exist. Everything is IR “variables”. The constraints (names being defined) are part of the IR . This gets a little bit tricky if it’s possible to define a name multiple times. What does mean in the instruction for ? Which definition does it refer to? In order to reason about the instruction , we have to keep around some context. This is non-trivial: requiring compiler writers to constantly truck around side-tables and update them as they do analysis is slow and error-prone. Fortunately, if we enforce some interesting rules, we can push that analysis work into one pass up-front… Static single assignment (SSA) was introduced by a bunch of folks at IBM (see my blog post about the different implementations). In SSA-based IRs, each variable can only be defined once. Put another way, a variable is its defining instruction; alternately, a variable and its defining instruction are addressed by the same name. The previous example is not valid SSA; has two definitions. If we turn the previous example into SSA, we can now use a different name for each instruction. This is related to the unique name assumption or the global names property: names do not depend on context. Now we can identify each different instruction by the variable it defines. This is useful in analysis… I’d be remiss if I did not mention continuation-passing style (CPS) based IRs (and in fact, I had forgotten in the original draft of the post). As an IR, CPS is normally used in the analysis and optimization of functional programs, for example in the OCaml and Racket compilers. It is not required, however; MLton, for example, uses SSA in its compiler for Standard ML. SSA and CPS can more or less represent the same programs, but they can each feel a natural fit for different languages (and different compiler authors). I don’t feel qualified to say much more here. For a more informed opinion, check out Andy Wingo’s approaching cps soup , especially the benefits and drawbacks near the end. Speaking of CPS, I took a class with Olin Shivers and he described abstract interpretation as “automated theorem finding”. Unlike theorem provers such as Lean and Rocq, where you have to manually prove the properties you want, static analysis finds interesting facts that already exist in your program (and optimizers use them to make your program faster). Your static analysis pass(es) can annotate your IR nodes with little bits of information such as: If your static analysis is over SSA, then generally the static analysis is easier and (potentially) storing facts is easier. This is due to this property called sparseness . Where a static analysis over non-SSA programs has to store facts about all variables at all program points , an analysis over SSA need only store facts about all variables, independent of context. I sometimes describe this as “pushing time through the IR” but I don’t know that that makes a ton of sense. Potentially more subtle here is that we could represent the above IR snippet as a list of tuples, where instructions are related via some other table (say, a “variable environment”): Instead, though, we could allocate an object for each instruction and let them refer to one another by pointer (or index, if using Rust or something). Then they directly refer to one another (no need for a side-table), which might be faster and more compact. We can re-create nice names as needed for printing. Then, when optimizing, we look up the type information of an operand by directly reading a field ( or similar). Another thing to note: when you start adding type information to your IR, you’re going to start asking type information questions in your analysis. Questions such as “what type is this instruction?”, where “type” could span a semilattice, and even refer to a specific run-time object by its pointer. In that case, it’s important to ask the right questions . For example: instructions are likely not the only opcodes that could produce specific objects; if you have an instruction like , for example, that burns a specific expected pointer into the generated code, the type (and therefore the pointer) will come from the instruction. The big idea is that types represent a different slice of your IR than the opcodes and should be treated as such. Anyway, SSA only stores type information about instructions and does not encode information that we might later learn in the IR. With basic SSA, there’s not a good way to encode refinements… Static single information (SSI) form gives us new ways to encode metadata about instructions (variables). It was introduced by C. Scott Ananian in 1999 in his MS thesis (PDF). (I also discussed it briefly in the Scrapscript IR post .) Consider the following SSA program (represented as pseudo-Python): is undefined at . is defined and an integer at . But then we do something interesting: we split control flow based on the run-time value of . We can take this split to add new and interesting information to . For non-sparse analysis, we can record some fact on the side. That’s fine. When doing a dataflow analysis, we can keep track of the fact that at , is nonnegative, and at , is negative. This is neat: we can then determine that all paths to this function return a positive integer. Importantly, does not override the existing known type of . Instead, it is a refinement: a set intersection. A lattice meet. The middle bit of a Venn diagram containing two overlapping circles, and . If we want to keep our information sparse, though, we have to add a new definition to the IR. This is complicated (choose which variables to split, replace all uses, to maintain SSA, etc) but gives us new places to store information inside the IR . It means that every time we refer to , we know that it is nonnegative and every time we refer to , we know that it is negative. This information is independent of context! I should note that you can get a lot of the benefits of SSI without going “full SSI”. There is no need to split every variable at every branch, nor add a special new merge instruction. Okay, so we can encode a lot of information very sparsely in the IR. That’s neat. It’s powerful. But we should also be mindful that even in this very sparse representation, we are encoding information implicitly that we may not want to: execution order. In a traditional CFG representation, the instructions are already scheduled , or ordered. Normally this comes from the programmer in the original source form and is faithfully maintained. We get data use edges in an IR like SSA, but the control information is left implicit. Some forms of IR, however, seek to reify both data and control dependencies into the IR itself. One such IR design is sea of nodes (SoN), which was originally designed by Cliff Click during his PhD. In sea of nodes, every instruction gets its own vertex in the graph. Instructions have use edges to their operands, which can be either data or some other ordering property (control, effects, etc). The main idea is that IR nodes are by default unordered and are only ordered later, after effect analysis has removed a bunch of use edges. Per Vognsen also notes that there is another motivating example of sea of nodes: in the previous SSI example, the cannot be validly hoisted above the check. In a “normal” IR, this is implicit in the ordering. In a sea of nodes world, this is explicitly marked with an edge from the to the . I think Graal, for example, calls these nodes “Pi nodes”. I think I need to re-read the original paper, read a modern implementation (I get the feeling it’s not done exactly the same way anymore), and then go write more about it later. For now, see Simple , by Cliff Click and friends. It is an implementation in Java and a little book to go with it. Design neighbors include value dependence graphs (VDG), value state dependence graphs (VSDG), region value state dependence graphs (RVSDG), and program dependence graphs (PDG). Speaking of Cliff Click, I once heard/read something he said that sounded really interesting. Roughly, it was “elaborate the full semantics of the operation into the IR and let the optimizer sort it out”. That is, “open code” or “inline” your semantics. For example, don’t emit code for a generic add operation that you later specialize: Instead, emit code that replicates the written semantics of the operation, whatever that is for your local language. This can include optimistic fast paths: This has the advantage that you may end up with fewer specialized rewrite rules because constant propagation and branch folding take care of these specializations “for free”. You can even attach probabilities to more or less likely branches to offer outlining hints in case all of this is never specialized. Sure, the downside of this is that the generated IR might be bigger, so your optimizer might be slower—or worse, that your resulting generated code at the end might be bigger. But outlining, deduplication (functionalization?), and probably some other clever methods can help here. Similarly, Maxime Chevalier-Boisvert and Marc Feeley write about this (PDF) in the context of basic block versioning. If the runtime’s generic add functions is written in IR, then callers to that function can specialize “through it” by calling it in different basic block contexts. That more or less gets you call-site specialization “for free”. See Figure 4 from their paper (lightly edited by me), where I think dollar-prefixed variable names indicate special functions known to the compiler: This is nice if you are starting a runtime from scratch or have resources to devote to re-writing chunks of the runtime in your IR. Then, even in a method JIT, you can get your inlined language semantics by function (partial) inlining. There’s probably more in this vein to be explored right now and probably more to invent in the future, too. Some other potentially interesting concepts to think about include: Thank you to Chris Fallin , Hunter Goldstein, and Per Vognsen for valuable feedback on drafts of this post.

0 views
baby steps 1 years ago

Rust turns 10

Today is the 10th anniversary of Rust’s 1.0 release . Pretty wild. As part of RustWeek there was a fantastic celebration and I had the honor of giving some remarks, both as a long-time project member but also as representing Amazon as a sponsor. I decided to post those remarks here on the blog. “It’s really quite amazing to see how far Rust has come. If I can take a moment to put on my sponsor hat, I’ve been at Amazon since 2021 now and I have to say, it’s been really cool to see the impact that Rust is having there up close and personal. “At this point, if you use an AWS service, you are almost certainly using something built in Rust. And how many of you watch videos on PrimeVideo? You’re watching videos on a Rust client, compiled to WebAssembly, and shipped to your device. “And of course it’s not just Amazon, it seems like all the time I’m finding out about this or that surprising place that Rust is being used. Just yesterday I really enjoyed hearing about how Rust was being used to build out the software for tabulating votes in the Netherlands elections . Love it. “On Tuesday, Matthias Endler and I did this live podcast recording. He asked me a question that has been rattling in my brain ever since, which was, ‘What was it like to work with Graydon?’ “For those who don’t know, Graydon Hoare is of course Rust’s legendary founder. He was also the creator of Monotone , which, along with systems like Git and Mercurial, was one of the crop of distributed source control systems that flowered in the early 2000s. So defintely someone who has had an impact over the years. “Anyway, I was thinking that, of all the things Graydon did, by far the most impactful one is that he articulated the right visions. And really, that’s the most important thing you can ask of a leader, that they set the right north star. For Rust, of course, I mean first and foremost the goal of creating ‘a systems programming language that won’t eat your laundry’. “The specifics of Rust have changed a LOT over the years, but the GOAL has stayed exactly the same. We wanted to replicate that productive, awesome feeling you get when using a language like Ocaml – but be able to build things like web browsers and kernels. ‘Yes, we can have nice things’, is how I often think of it. I like that saying also because I think it captures something else about Rust, which is trying to defy the ‘common wisdom’ about what the tradeoffs have to be. “But there’s another North Star that I’m grateful to Graydon for. From the beginning, he recognized the importance of building the right culture around the language, one committed to ‘providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, disability, nationality, or other similar characteristic’, one where being ‘kind and courteous’ was prioritized, and one that recognized ’there is seldom a right answer’ – that ‘people have differences of opinion’ and that ’every design or implementation choice carries a trade-off’. “Some of you will probably have recognized that all of these phrases are taken straight from Rust’s Code of Conduct which, to my knowledge, was written by Graydon. I’ve always liked it because it covers not only treating people in a respectful way – something which really ought to be table stakes for any group, in my opinion – but also things more specific to a software project, like the recognition of design trade-offs. “Anyway, so thanks Graydon, for giving Rust a solid set of north stars to live up to. Not to mention for the keyword. Raise your glass! “For myself, a big part of what drew me to Rust was the chance to work in a truly open-source fashion. I had done a bit of open source contribution – I wrote an extension to the ASM bytecode library, I worked some on PyPy, a really cool Python compiler – and I loved that feeling of collaboration. “I think at this point I’ve come to see both the pros and cons of open source – and I can say for certain that Rust would never be the language it is if it had been built in a closed source fashion. Our North Star may not have changed but oh my gosh the path we took to get there has changed a LOT. So many of the great ideas in Rust came not from the core team but from users hitting limits, or from one-off suggestions on IRC or Discord or Zulip or whatever chat forum we were using at that particular time. “I wanted to sit down and try to cite a bunch of examples of influential people but I quickly found the list was getting ridiculously long – do we go all the way back, like the way Brian Anderson built out the infrastructure as a kind of quick hack, but one that lasts to this day? Do we cite folks like Sophia Turner and Esteban Kuber’s work on error messages? Or do we look at the many people stretching the definition of what Rust is today … the reality is, once you start, you just can’t stop. “So instead I want to share what I consider to be an amusing story, one that is very Rust somehow. Some of you may have heard that in 2024 the ACM, the major academic organization for computer science, awarded their SIGPLAN Software Award to Rust. A big honor, to be sure. But it caused us a bit of a problem – what names should be on there? One of the organizers emailed me, Graydon, and a few other long-time contributors to ask us our opinion. And what do you think happened? Of course, we couldn’t decide. We kept coming up with different sets of people, some of them absurdly large – like thousands of names – others absurdly short, like none at all. Eventually we kicked it over to the Rust Leadership Council to decide. Thankfully they came up with a decent list somehow. “In any case, I just felt that was the most Rust of all problems: having great success but not being able to decide who should take credit. The reality is there is no perfect list – every single person who got named on that award richly deserves it, but so do a bunch of people who aren’t on the list. That’s why the list ends with All Rust Contributors, Past and Present – and so a big shout out to everyone involved, covering the compiler, the tooling, cargo, rustfmt, clippy, core libraries, and of course organizational work. On that note, hats off to Mara, Erik Jonkers, and the RustNL team that put on this great event. You all are what makes Rust what it is. “Speaking for myself, I think Rust’s penchant to re-imagine itself, while staying true to that original north star, is the thing I love the most. ‘Stability without stagnation’ is our most important value. The way I see it, as soon as a language stops evolving, it starts to die. Myself, I look forward to Rust getting to a ripe old age, interoperating with its newer siblings and its older aunts and uncles, part of the ‘cool kids club’ of widely used programming languages for years to come. And hey, maybe we’ll be the cool older relative some day, the one who works in a bank but, when you talk to them, you find out they were a rock-and-roll star back in the day. “But I get ahead of myself. Before Rust can get there, I still think we’ve some work to do. And on that note I want to say one other thing – for those of us who work on Rust itself, we spend a lot of time looking at the things that are wrong – the bugs that haven’t been fixed, the parts of Rust that feel unergonomic and awkward, the RFC threads that seem to just keep going and going, whatever it is. Sometimes it feels like that’s ALL Rust is – a stream of problems and things not working right. “I’ve found there’s really only one antidote, which is getting out and talking to Rust users – and conferences are one of the best ways to do that. That’s when you realize that Rust really is something special. So I do want to take a moment to thank all of you Rust users who are here today. It’s really awesome to see the things you all are building with Rust and to remember that, in the end, this is what it’s all about: empowering people to build, and rebuild, the foundational software we use every day. Or just to ‘hack without fear’, as Felix Klock legendarily put it. “So yeah, to hacking!”

0 views
sunshowers 1 years ago

Debugging a rustc segfault on illumos

At Oxide , we use Helios as the base OS for the cloud computers we sell. Helios is a distribution of illumos , a Unix-based operating system descended from Solaris. As someone who learned illumos on the job, I’ve been really impressed by the powerful debugging tools it provides. I had a chance to use some of them recently to track down a segmentation fault in the Rust compiler, with the help of several of my colleagues. I learned a lot from the process, and I thought I’d write about it! I’m writing this post for an audience of curious technologists who aren’t necessarily familiar with systems work. If you’re an experienced systems developer, parts of it are likely familiar to you—feel free to skip over them. A couple of weeks ago, I wanted to make a change to the Rust standard library on illumos. I logged into my illumos box and cloned the Rust repository (revision ). Following the setup instructions , I configured the build system with the build profile. When I went to run , I saw an error with the following output: Quite concerning! Like any good technologist I tried running the command again. But the segfault seemed to be completely deterministic: the program would crash while compiling every time. Coincidentally, we had our fortnightly “Rust @ Oxide” virtual meetup at around that time. There wasn’t much to discuss there, so we turned that meeting into a debugging session. (I love how my coworkers get excited about debugging strange issues.) Like the compilers for many other languages, the Rust compiler is written in the language it is intending to compile (in this case, Rust). In other words, the Rust compiler is self-hosting . Any self-hosting compiler needs to answer the question: how in the world do you compile the compiler if you don’t already have a working compiler? This is known as the bootstrapping problem . There are several ways to address the problem, but the two most common are: Use the previous version of the compiler. In other words, use version N-1 of the compiler to compile version N. For example, use Rust 1.75 to compile Rust 1.76. The earliest versions of Rust were written in Ocaml. So if you’re spinning up Rust on a brand new platform and have an Ocaml compiler available, you can actually start from there and effectively create your own lineage of compilers. There are also implementations of Rust in other languages, like in C++, which can be used to build some (typically pretty old) version of the compiler. Interestingly, these other implementations don’t need to be perfect—for example, since they’re only used to compile code that’s known to be valid, they don’t need to handle errors well. That’s a large chunk of the complexity of a real compiler. Cross-compile from another platform. As a shortcut, if you have a way to cross-compile code from another platform, you can use that to set up the initial compiler. This is the most common method for setting up Rust on a new platform. (But note that method 1 must be used on at least one platform.) While bootstrapping from the previous version of Rust, the toolchain follows a series of stages , ranging from stage 0 to stage 2 . In our case, since we’re working with the standard library we’re only concerned with stage 0 : the standard library compiled with the previous version of . That is the build process that crashed. The first thing to find is the version of that’s crashing. There are a few ways to find the compiler, but a simple command works well: This command finds at . Let’s ask it for its version: Can the bug be reproduced independently of the Rust toolchain? The toolchain does all sorts of non-standard things, so it’s worth checking. The output says , so let’s try building that separately. Again, there are a few ways to do this, but the easiest is to make a simple Cargo project that depends on the crate. And then run . I didn’t have rustc 1.80.0 beta 1 on the machine, so I tried with the 1.80.0 release: Yep, it crashes in the same spot. This is a minimal-enough example, so let’s work with this. When a program crashes, systems are typically configured to generate a core dump , also known as a core file. The first step while debugging any crash is to ensure that core dumps are generated, and then to find one to examine it. On illumos, many of the system-level administration tools are called . The tool for managing core files is called . Let’s run that: This suggests that core “per-process core dumps” are enabled. The lack of a pattern indicates that the defaults are used. Generally, on Unix systems the default is to generate a file named in the current directory of the crashing process. A simple in our little test project doesn’t show a file, which means that it might be elsewhere. Let’s just do a global for it. This showed a few files on my system, including: . Bingo! That looks like a hit. (Why is it in the registry? Because when compiling a crate, Cargo sets the current working directory of the child process to the crate’s directory.) The next step is to move the file into another directory 1 . After doing that, let’s start examining it. The best way to examine a core file on illumos is with the Modular Debugger, . is a powerful tool that can be used to inspect the state of both live and dead processes, as well as the kernel itself. Using with the core file is simple: just run . The first step is to enable symbol demangling 2 . The command to do that in is , so let’s run that: (The output says “C++”, but illumos’s demangler can handle Rust symbols, too.) Let’s look at the CPU registers now. A register stores a small amount of data that the CPU can access very quickly. Core files typically have the contents of registers at the time of the crash, which can be very useful for debugging. In , the command to print out registers is or . Here’s the output: All right, there’s a lot going on here. A full accounting of the registers on x86-64 is beyond the scope of this post, but if you’re interested here’s a quick summary . The most important registers here are , , and . All three of these are 64-bit addresses. is the instruction pointer , also known as the program counter . is a special register that points to the next instruction to be executed. The CPU uses to keep track of where it is in the program. is the stack pointer . The call stack is a region of memory that is used to store function call information and local variables. The stack pointer points to the head of the stack. Note that on most architectures including x86-64, the stack grows down in memory: when a function is called, a new stack frame is set up and the stack pointer is decremented by however much space the function needs. is the base pointer , more commonly known as the frame pointer . It points to the base of the current stack frame 3 . We can also look at the call stack via the command. The stack turns out to be enormous ( full output ): (The is used to send the output to a shell command, in this case one that counts the number of lines.) It looks like the crash is in the parser. (Notably, the crash is while compiling a crate called , which suggests automatic code generation. Generated code often tends to stress the parser in ways that manually written code does not.) Based on the call stack, it looks like the parser is recursive in nature. A quick Google search confirms that the parser is a “simple hand-written recursive descent parser”. This isn’t surprising, since most production parsers are written this way. (For example, is also a recursive descent parser.) Turning our attention to the instruction pointer , we can use the command to disassemble the function at that address. ( Full output ; the flag ensures that addresses are not converted to very long function names.) So it looks like the crash is happening in a instruction to another function, . (Keep in mind that this information could be completely unreliable! The stack might be corrupted, the registers might be wrong, and so on. But it’s what we have for now.) On virtual memory systems , which includes all modern desktop and server systems, each process gets the illusion that it has a very large amount of memory all to itself. This is called the address space of a process. The instructions, the call stack, and the heap all get their own regions of addresses in that space, called memory mappings . The 64-bit addresses that we saw earlier are all part of the address space. has a command called to look up which part of memory an address is at. Let’s look at the stack pointer first: This tells us that the address is in the range to . This is a small 4 KiB range. What about the frame pointer? This appears to be in a different range. In this case, the ending address is (note the , not the !). This address is bytes away from the starting address. That is equal to 1028 KiB , or 1 MiB + 4 KiB page 4 . Something else that’s relevant here is what permissions each range of addresses has. Like files on Unix, a block of virtual memory can have read , write , or execute permissions. (In this case, execute means that it is valid for the instruction pointer to point here 5 .) On illumos, a tool called can show these spaces. works on both live processes and core files. Running shows the permissions for the addresses we’re interested in ( full output ): The 1028 KiB range is read-write, and the 4 KiB range above that doesn’t have any permissions whatsoever. This would explain the segfault . A segfault is an attempt to operate on a part of memory that the program doesn’t have permissions for. Attempting to read from or write to memory which has no permissions is an example of that. At this point, we have enough information to come up with a theory: But there are also other bits of evidence that this theory doesn’t explain, or even cuts against. (This is what makes post-mortem debugging exciting! There are often contradictory-seeming pieces of information that need to be explained.) The memory is marked or . That’s not how call stacks are supposed to be marked! In the output, there’s a line which says: So you’d expect call stacks to be marked with , not . Why is the size of the allocation 1028 KiB? You’d generally expect stack sizes to be a round power of two. Isn’t 1028 KiB kind of small? The thread is a non-main thread, and the default stack size for Rust threads is 2 MiB . Why is our thread ~1 MiB and not 2 MiB? On Unix platforms, for the main thread, the call stack size is determined by (in KiB). On my illumos machine, this printed , indicating a 10 MiB call stack. For child threads, the call stack size is determined by whatever created them. For Rust, the default is 2 MiB. Why doesn’t this crash happen on other platforms? If this is a crash in the parser, one would ordinarily expect it to arise everywhere. Yet it doesn’t seem to occur on Linux, macOS, or Windows. What’s special about illumos? Setting doesn’t help. Rust-created thread stack sizes can be configured via the environment variable . If we try to use that: It turns out that crashes at exactly the same spot. That’s really strange! It is possible that the stack size was overridden at thread creation time. The documentation for says: “Note that setting will override this.” But that seems unlikely. Looking towards the bottom of the call stack, there’s something really strange : Notice the jump in addresses from to ? Normally, stack addresses are decremented as new functions are called: the number goes down. In this case the stack address is incremented . The number went up. Strange. Also notice that this coincides with the use of a function called . Now that’s a real lead! What part of memory is in? says: So this address is part of the stack for thread 3. agrees : What is ? Time for some googling! Per the documentation , is: A library to help grow the stack when it runs out of space. This is an implementation of manually instrumented segmented stacks where points in a program’s control flow are annotated with “maybe grow the stack here”. Each point of annotation indicates how far away from the end of the stack it’s allowed to be, plus the amount of stack to allocate if it does reach the end. Because the parser is recursive, it is susceptible to call stack exhaustion. The use of is supposed to prevent, or at least mitigate, that. How does work? The library has a pretty simple API : The developer is expected to intersperse calls to within their recursive function. If less than bytes of stack space remain, will allocate a new segment of bytes, and run with the stack pointer pointing to the new segment. How does rustc use ? The code is in this file . The code requests an additional 1 MiB stack with a red zone of 100 KiB. Why did create a new stack segment? In our case, the call is at the very bottom of the stack, when plenty of space should be available, so ordinarily should not need to allocate a new segment. Why did it do so here? The answer is in ’s source code . There is code to guess the stack size on many platforms. But it isn’t enabled on illumos: always returns . With this information in hand, we can flesh out our call stack exhaustion theory: Some file in was triggering the crash by requiring more than 1 MiB of stack space. Had this bug occurred on other platforms like Linux, this issue would have been a showstopper. However, it wasn’t visible on those platforms because: didn’t call enough! In order for it to work, needs to be interspersed throughout the recursive code. But some recursive parts did not appear to have called it. (It is somewhat ironic that , a library meant to prevent call stack exhaustion, was actively making life worse here.) Where does the 1028 KiB come from? Looking at the source code : It looks like first computes the number of requested pages by dividing the requested stack size by the page size, rounding up. Then it adds 2 to that. In our case: This explains both the 1028 KiB allocation (one guard page after the stack), and the 4 KiB guard page we’re crashing at (one guard page before the stack). If the issue is that a 1 MiB stack isn’t enough, it should be possible to reproduce this on other platforms by setting their stack size to something smaller than the 2 MiB default. With a stack size <= 1 MiB, we would expect that: Let’s try to compile on Linux with a reduced stack size. This does crash as expected. The full output is here . Some of the symbols are missing, but the crash does seem to be in parser code. (At this point, we could have gone further and tried to make a debug-assertions build of – but it was already pretty clear why the crash was happening.) Call stack exhaustion in the parser suggests that the crash is happening in some kind of large, automatically generated file. But what file is it? It’s hard to tell by looking at the core file itself, but we have another dimension of debugging at hand: syscall tracers! These tools print out all the syscalls made by a process. Most OSes have some means to trace syscalls: on Linux, on macOS, Process Monitor on Windows, and on illumos 7 . Since we’re interested in file reads, we can try filtering it down to the and syscalls . You need to open a file to read it, after all. (Alternatively, we can also simply not filter out any syscalls, dump the entire trace to a file, and then look at it afterwards.) On illumos, we tell to run , filtering syscalls to and ( ), and following child processes ( ): This prints out every file that the child tries to open ( full output ): It looks like the crash is in a file called in the directory. With Cargo, a file being in an directory is a pretty strong indication that it is generated by a build script. On Linux, a similar command is: This command also blames the same file, . What does this file look like, anyway? Here’s my copy. It’s pretty big and deeply nested! It does look large and complex enough to trigger call stack exhaustion. Syscall traces would definitely be somewhat harder to get if the crash weren’t so easily reproducible. Someone smarter than me should write about how to figure this out using just the core file. The file’s fully loaded into memory so it seems like it should be possible. Going back to the beginning: the reason I went down this adventure was because I wanted to make an unrelated change to the Rust standard library. But the stage 0 compiler being broken meant that it was impossible to get to the point where I could build the standard library as-is, let alone test that change. How can we work around this? Well, going back to basics, where did the stage 0 compiler come from? It came from Rust’s CI, and it wasn’t actually built on illumos! (Partly because there’s no publicly-available CI system running illumos.) Instead, it was cross-compiled from Linux to illumos. Based on this, my coworker Joshua suggested that I try and do whatever Rust’s CI does to build a stage 0 compiler for illumos. Rust’s CI uses a set of Docker images to build distribution artifacts. In theory, building a patched rustc should be as simple as running these commands on my Linux machine: In reality, there were some Docker permissions issues due to which I had to make a couple of changes to the script. Overall, though, it was quite simple. Here’s the patch I built the compiler with, including the changes to the CI scripts. The result of building the compiler was a set of files, just like the ones published by Rust’s CI . After copying the files over to my illumos machine, I wasn’t sure which tarballs to extract. So I made a small change to the bootstrap script to use my patched tarballs. With this patch, I was able to successfully build Rust’s standard library on illumos and test my changes. Hooray! ( Here’s what I was trying to test.) Update 2024-08-05: After this post was published, jyn pointed out on Mastodon that is actually optional, and that I could have also worked around the issue by disabling it in the build system’s . Thanks! The bug occurred due to a combination of several factors. It also revealed a few other issues, such as the lack of an environment variable workaround and some missing error reporting. Here are some ways we can make the situation better, and help us have an easier time debugging similar issues in the future. isn’t using enough. The basic problem underneath it all is that the part of the parser that triggered the bug wasn’t calling often enough to make new stack segments. should be calling more than it is today. cannot detect the stack size on illumos. This is something that we should fix in , but this is actually a secondary issue here. On other platforms, ’s ability to detect the stack size was masking the bug. Fixing this requires two changes: -created segments don’t print a nice message on stack exhaustion. This is a bit ironic because is supposed to prevent stack exhaustion. But when it does happen, it would be nice if printed out a message like standard Rust does. On illumos, the Rust runtime doesn’t print a message on stack exhaustion. Separate from the previous point, on illumos the Rust runtime doesn’t print a message on stack exhaustion even when using native stacks. Rust’s CI doesn’t run on illumos. At Oxide, we have an existential dependency on Rust targeting illumos. Even a shadow CI that ran on nightly releases would have caught this issue right away. We’re discussing the possibilities for this internally; stay tuned! segment sizes can’t be controlled via the environment. Being able to control stack sizes with is a great way to work around issues. It doesn’t appear that segment sizes can be controlled in this manner. Maybe that functionality should be added to , or to itself? Maybe a crater run with a smaller stack size? It would be interesting to see if there are other parts of the Rust codebase that need to call more as well. suggests disabling optional components. Since was an optional component that can be disabled, the tooling could notice if a build failed in such a component, and recommend disabling that component. Added 2024-08-05, suggested by jyn . To me, this is the most exciting part of debugging: what kinds of changes can we make, both specific and systemic ones, to make life easier for our future selves? This was a really fun debugging experience because I got to learn about several illumos debugging tools, and also because we could synthesize information from several sources to figure out a complex issue. (Thankfully, the root cause was straightforward, with no memory corruption or other “spooky action at a distance” involved.) Debugging this was a real team effort. I couldn’t have done it without the assistance of several of my exceptional colleagues. In no particular order: Thanks to all of you! I neglected to do this during my own debugging session, which led to some confusion when I re-ran the process and found that the core file had been overwritten.  ↩︎ Name mangling is a big topic of its own, but the short version is that the Rust compiler uses an algorithm to encode function names into the binary. The encoding is designed to be reversible, and the process of doing so is called demangling. (Other languages like C++ do name mangling, too.)  ↩︎ You might have heard about “frame pointer omission”, which is a technique to infer the base of stack frames rather than storing it in explicitly. In this case, the frame pointer is not omitted.  ↩︎ A page is the smallest amount of physical memory that can be atomically mapped to virtual memory. On x86-64, the page size is virtually always 4 KiB.  ↩︎ Memory being both writable and executable is dangerous, and modern systems do not permit this by default for security reasons. Some platforms like iOS even make it impossible for memory to be writable and executable, unless the platform holder gives you the corresponding permissions.  ↩︎ This is generally known as a “stack overflow”, but that term can also mean a stack-based buffer overflow . Throughout this document, we use “call stack exhaustion” to avoid confusion.  ↩︎ There is likely some way to get itself to print out which files it opened, but the beauty of system call tracers is that you don’t need to know anything about the program you’re tracing.  ↩︎ Use the previous version of the compiler. In other words, use version N-1 of the compiler to compile version N. For example, use Rust 1.75 to compile Rust 1.76. From where do you begin, though? The earliest versions of Rust were written in Ocaml. So if you’re spinning up Rust on a brand new platform and have an Ocaml compiler available, you can actually start from there and effectively create your own lineage of compilers. There are also implementations of Rust in other languages, like in C++, which can be used to build some (typically pretty old) version of the compiler. Interestingly, these other implementations don’t need to be perfect—for example, since they’re only used to compile code that’s known to be valid, they don’t need to handle errors well. That’s a large chunk of the complexity of a real compiler. Cross-compile from another platform. As a shortcut, if you have a way to cross-compile code from another platform, you can use that to set up the initial compiler. This is the most common method for setting up Rust on a new platform. (But note that method 1 must be used on at least one platform.) is the instruction pointer , also known as the program counter . is a special register that points to the next instruction to be executed. The CPU uses to keep track of where it is in the program. is the stack pointer . The call stack is a region of memory that is used to store function call information and local variables. The stack pointer points to the head of the stack. Note that on most architectures including x86-64, the stack grows down in memory: when a function is called, a new stack frame is set up and the stack pointer is decremented by however much space the function needs. is the base pointer , more commonly known as the frame pointer . It points to the base of the current stack frame 3 . The thread had a call stack of 1028 KiB available to it, starting at . The call stack pointer was at (only = 320 bytes away), and it tried to create a frame of size (1312) bytes, at . This caused the call stack to be exhausted : the thread ran out of space 6 . When the thread ran out of space, it indexed into a 4 KiB section known as a guard page . The thread did not have any permissions to operate on the page, and was in fact designed to cause a segfault if accessed in any way. The program then (correctly) segfaulted. The memory is marked or . That’s not how call stacks are supposed to be marked! In the output, there’s a line which says: So you’d expect call stacks to be marked with , not . Why is the size of the allocation 1028 KiB? You’d generally expect stack sizes to be a round power of two. Isn’t 1028 KiB kind of small? The thread is a non-main thread, and the default stack size for Rust threads is 2 MiB . Why is our thread ~1 MiB and not 2 MiB? How are call stack sizes determined? On Unix platforms, for the main thread, the call stack size is determined by (in KiB). On my illumos machine, this printed , indicating a 10 MiB call stack. For child threads, the call stack size is determined by whatever created them. For Rust, the default is 2 MiB. Why doesn’t this crash happen on other platforms? If this is a crash in the parser, one would ordinarily expect it to arise everywhere. Yet it doesn’t seem to occur on Linux, macOS, or Windows. What’s special about illumos? Setting doesn’t help. Rust-created thread stack sizes can be configured via the environment variable . If we try to use that: It turns out that crashes at exactly the same spot. That’s really strange! It is possible that the stack size was overridden at thread creation time. The documentation for says: “Note that setting will override this.” But that seems unlikely. Some file in was triggering the crash by requiring more than 1 MiB of stack space. The parser running against needed more than 1 MiB of stack space, but less than 2 MiB. Had this bug occurred on other platforms like Linux, this issue would have been a showstopper. However, it wasn’t visible on those platforms because: Threads created by Rust use a 2 MiB stack by default. requested that create a 1 MiB stack segment, but only if less than 100 KiB of stack space was left. On the other platforms, could see that well over 100 KiB of stack space was left, and so it did not allocate a new segment. On illumos, could not see how much stack was left, and so it allocated a new 1 MiB segment. This 1 MiB stack was simply not enough to parse . didn’t call enough! In order for it to work, needs to be interspersed throughout the recursive code. But some recursive parts did not appear to have called it. The requested stack size is 1 MiB. With 4 KiB pages, this works out to 256 pages. then requests 256 + 2 = 258 pages, which is 1032 KiB. calls as before. There are two possibilities: either decides there is enough stack space and doesn’t create a new segment, or it decides there isn’t enough and does create a new 1 MiB segment. In either case, 1 MiB is simply not enough to parse , and the program crashes. isn’t using enough. The basic problem underneath it all is that the part of the parser that triggered the bug wasn’t calling often enough to make new stack segments. should be calling more than it is today. Filed as rust-lang/rust#128422 . cannot detect the stack size on illumos. This is something that we should fix in , but this is actually a secondary issue here. On other platforms, ’s ability to detect the stack size was masking the bug. Fixing this requires two changes: A PR to to add the function to it. A PR to to use this function to detect the stack size on illumos. -created segments don’t print a nice message on stack exhaustion. This is a bit ironic because is supposed to prevent stack exhaustion. But when it does happen, it would be nice if printed out a message like standard Rust does. This is rust-lang/stacker#59 . On illumos, the Rust runtime doesn’t print a message on stack exhaustion. Separate from the previous point, on illumos the Rust runtime doesn’t print a message on stack exhaustion even when using native stacks. Filed as rust-lang/rust#128568 . Rust’s CI doesn’t run on illumos. At Oxide, we have an existential dependency on Rust targeting illumos. Even a shadow CI that ran on nightly releases would have caught this issue right away. We’re discussing the possibilities for this internally; stay tuned! segment sizes can’t be controlled via the environment. Being able to control stack sizes with is a great way to work around issues. It doesn’t appear that segment sizes can be controlled in this manner. Maybe that functionality should be added to , or to itself? Opened a discussion on internals.rust-lang.org . Maybe a crater run with a smaller stack size? It would be interesting to see if there are other parts of the Rust codebase that need to call more as well. suggests disabling optional components. Since was an optional component that can be disabled, the tooling could notice if a build failed in such a component, and recommend disabling that component. Added 2024-08-05, suggested by jyn . Joshua M. Clulow Matt Keeter Cliff Biffle Steve Klabnik artemis everfree I neglected to do this during my own debugging session, which led to some confusion when I re-ran the process and found that the core file had been overwritten.  ↩︎ Name mangling is a big topic of its own, but the short version is that the Rust compiler uses an algorithm to encode function names into the binary. The encoding is designed to be reversible, and the process of doing so is called demangling. (Other languages like C++ do name mangling, too.)  ↩︎ You might have heard about “frame pointer omission”, which is a technique to infer the base of stack frames rather than storing it in explicitly. In this case, the frame pointer is not omitted.  ↩︎ A page is the smallest amount of physical memory that can be atomically mapped to virtual memory. On x86-64, the page size is virtually always 4 KiB.  ↩︎ Memory being both writable and executable is dangerous, and modern systems do not permit this by default for security reasons. Some platforms like iOS even make it impossible for memory to be writable and executable, unless the platform holder gives you the corresponding permissions.  ↩︎ This is generally known as a “stack overflow”, but that term can also mean a stack-based buffer overflow . Throughout this document, we use “call stack exhaustion” to avoid confusion.  ↩︎ There is likely some way to get itself to print out which files it opened, but the beauty of system call tracers is that you don’t need to know anything about the program you’re tracing.  ↩︎

0 views
Emil Privér 1 years ago

Why I Like Ocaml

According to my Linkedin profile, I have been writing code for a company for almost 6 years. During this time, I have worked on PHP and Wordpress projects, built e-commerce websites using NextJS and JavaScript, written small backends in Python with Django/Flask/Fastapi, and developed fintech systems in GO, among other things. I have come to realize that I value a good type system and prefer writing code in a more functional way rather than using object-oriented programming. For example, in GO, I prefer passing in arguments rather than creating a method. This is why I will be discussing OCaml in this article. If you are not familiar with the language OCaml or need a brief overview of it, I recommend reading my post OCaml introduction before continuing with this post. It will help you better understand the topic I am discussing. Almost every time I ask someone what they like about OCaml, they often say “oh, the type system is really nice” or “I really like the Hindley-Milner type system.” When I ask new OCaml developers what they like about the language, they often say “This type system is really nice, Typescript’s type system is actually quite garbage.” I am not surprised that these people say this, as I agree 100%. I really enjoy the Hindley-Milner type system and I think this is also the biggest reason why I write in this language. A good type system can make a huge difference for your developer experience. For those who may not be familiar with the Hindley-Milner type system, it can be described as a system where you write a piece of program with strict types, but you are not required to explicitly state the types. Instead, the type is inferred based on how the variable is used. Let’s look at some code to demonstrate what I mean. In GO, you would be required to define the type of the arguments: However, in OCaml, you don’t need to specify the type: Since expects to receive a string, the signature for will be: But it’s not just for arguments, it’s also used when returning a value. This function will not compile because we are trying to return a string as the first value and later an integer. I also want to provide a larger example of the Hindley-Milner type system: The signature for this piece of code will be: In this example, we create a new module where we expose 3 functions: make, print_car_age, and print_car_name. We also define a type called . One thing to note in the code is that the type is only defined once, as OCaml infers the type within the functions since is a type within this scope. OCaml playground for this code Something important to note before concluding this section is that you can define both the argument types and return types for your function. The next topic is pattern matching. I really enjoy pattern matching in programming languages. I have written a lot of Rust, and pattern matching is something I use when I write Rust. Rich pattern matching is beneficial as it eliminates the need for many if statements. Additionally, in OCaml, you are required to handle every case of the match statement. For example, in the code below: In the code above, I am required to include the last match case because we have not handled every case. For example, what should the compiler do if the is Adam? The example above is very simple. We can also match on an integer and perform different actions based on the number value. For instance, we can determine if someone is allowed to enter the party using pattern matching. OCaml playground But the reason I mention variants in this section is that variants and pattern matching go quite nicely hand in hand. A variant is like an enumeration with more features, and I will show you what I mean. We can use them as a basic enumeration, which could look like this: This now means that we can do different things depending on this type: But I did mention that variants are similar to enumeration with additional features, allowing for the assignment of a type to the variant. Now that we have added types to our variants and included , we are able to adjust our pattern matching as follows: OCaml Playground We can now assign a value to the variant and use it in pattern matching to print different values. As you can see, I am not forced to add a value to every variant. For instance, I do not need a type on so I simply don’t add it. I often use variants, such as in DBCaml where I use variants to retrieve responses from a database. For example, I return if I did not receive any rows back, but no error. OCaml also comes with Exhaustiveness Checking, meaning that if we don’t check each case in a pattern matching, we will get an error. For instance, if we forget to add to the pattern matching, OCaml will throw an error at compile time. The next topic is operators and specific binding operators. OCaml has more types of operators, but binding operators are something I use in every project. A binding could be described as something that extends how works in OCaml by adding extra logic before storing the value in memory with . I’ll show you: This code simply takes the value “Emil” and stores it in memory, then assigns the memory reference to the variable hello. However, we can extend this functionality with a binding operator. For instance, if we don’t want to use a lot of match statements on the return value of a function, we can bind so it checks the value and if the value is an error, it bubbles up the error. This allows me to reduce the amount of code I write while maintaining the same functionality. In the code above, one of the variables is an , which means that the binding will return the error instead of returning the first name and last name. I really like the concept of functional programming, such as immutability and avoiding side-effects as much as possible. However, I believe that a purely functional programming language could force us to write code in a way that becomes too complex. This is where I think OCaml does a good job. OCaml is clearly designed to be a functional language, but it allows for updating existing values rather than always returning new values. Immutability means that you cannot change an already existing value and must create a new value instead. I have written about the Concepts of Functional Programming and recommend reading it if you want to learn more. One example where functional programming might make the code more complex is when creating a reader to read some bytes. If we strictly follow the rule of immutability, we would need to return new bytes instead of updating existing ones. This could lead to inefficiencies in terms of memory usage. Just to give an example of how to mutate an existing value in OCaml, I have created an example. In the code below, I am updating the age by 1 as it is the user’s birthday: What I mean by “it’s functional on easy mode” is simply that the language is designed to be a functional language, but you are not forced to strictly adhere to functional programming rules. It is clear to me that a good type system can greatly improve the developer experience. I particularly appreciate OCaml’s type system, as well as its and types, which I use frequently. In languages like Haskell, you can extend the type system significantly, to the point where you can write an entire application using only types. However, I believe that this can lead to overly complex code. This is another aspect of OCaml that I appreciate - it has a strong type system, but there are limitations on how far you can extend it. I hope you enjoyed this article. If you are interested in joining a community of people who also enjoy functional programming, I recommend joining this Discord server.

0 views