Latest Posts (10 found)
matklad 1 months ago

Look Out For Bugs

One of my biggest mid-career shifts in how I write code was internalizing the idea from this post: Don’t Write Bugs Historically, I approached coding with an iteration-focused mindset — you write a draft version of a program, you set up some kind of a test to verify that it does what you want it to do, and then you just quickly iterate on your draft until the result passes all the checks. This was a great approach when I was only learning to code, as it allowed me to iterate past the things which were not relevant for me at that point, and focus on what matters. Who cares if it is or in the “паблик статик войд мэйн стринг а-эр-джи-эс”, it’s just some obscure magic spell anyway, and completely irrelevant to the maze-traversing thingy I am working on! Carrying over this approach past the learning phase was a mistake. As Lawrence points out, while you can spend time chasing bugs in the freshly written code, it is possible to dramatically cut the amount of bugs you introduce in the first place, if you focus on optimizing that (and not just the iteration time). It felt (and still feels) like a superpower! But there’s already a perfectly fine article about not making bugs, so I am not going to duplicate it. Instead, I want to share a related, but different super power: You can find bugs by just reading code. I remember feeling this superpower for the first time. I was investigating various rope implementations, and, as a part of that, I looked at the , the implementation powering IntelliJ, very old and battle tested code. And, by just reading the code, I found a bug, since fixed . It wasn’t hard, the original code is just 500 lines of verbose Java (yup, that’s all that you need for a production rope). And I wasn’t even trying to find a bug, it just sort-of jumped out at me while I was trying to understand how the code works. That is, you can find some existing piece of software, carefully skim through implementation, and discover real problems that can be fixed. You can do this to your software as well! By just re-reading a module you wrote last year, you might find subtle problems. I regularly discover TigerBeetle issues by just covering this or that topic on IronBeetle : bug discovered live , fixed , and PR merged . Here are some tips for getting better at this: The key is careful, slow reading. What you actually are doing is building the mental model of a program inside your head. Reading the source code is just an instrument for achieving that goal. I can’t emphasize this enough: programming is all about building a precise understanding inside your mind, and then looking for the diff between your brain and what’s in git. Don’t dodge an opportunity to read more of the code. If you are reviewing a PR, don’t review just the diff, review the entire subsystem. When writing code, don’t hesitate to stop and to probe and feel the context around. Go for or to understand the historical “why” of the code. When reading, mostly ignore the textual order, don’t just read each source file top-down. Instead, use these two other frames: Start at or subsystem equivalent, and use “goto definition” to follow an imaginary program counter. Identify the key data structures and fields, and search for all places where they are created and modified. You want to see a slice across space and time, state and control flow (c.f. Concurrent Expression Problem ). Just earlier today I used the second trick to debug an issue for which I haven’t got a repro. I identified as the key assignment that was recently introduced, then ctrl + f for , and that immediately revealed a gap in my mental model. Note how this was helped by the fact that the thing in question, , was always called that in the source code! If your language allows it, avoid , use proper names. Identify and collect specific error-prone patterns or general smells in the code. In Zig, if there’s an allocator and a in the same scope, you need to be very careful . If there’s an isolated tricky function, it’s probably fine. If there’s a tricky interaction between functions, it is a smell, and some bugs are lurking there. Bottom line: reading the code is surprisingly efficient at proactively revealing problems. Create space for calm reading. When reading, find ways to build mental models quickly, this is not entirely trivial.

0 views
matklad 1 months ago

Vibe Coding Terminal Editor

I “wrote” a small tool for myself as my biannual routine check of where llms are currently at. I think I’ve learned a bunch from this exercise. This is frustrating! I don’t want to learn by trial and error, I’d rather read someone’s blog post with lessons learned. Sadly, most of the writing on the topic that percolates to me tends to be high-level — easy to nod along while reading, but hard to extract actionable lessons. So this is what I want to do here, list specific tricks learned. Let me quickly introduce the project. It’s a VS Code extension that allows me to run “shell” inside my normal editor widget, such that the output is normal text buffer where all standard motion/editing commands work. So I can “goto definition” on paths printed as a part of backtrace, use multiple cursors to copy compiler’s suggestions, or just PageUp / PageDown to scroll the output. If you are familiar with Emacs, it’s Eshell , just worse: I now use to launch most of my compilation commands, as it has several niceties on top of what my normal shell provides. For example, by default only the last 50 lines of output are shown, but I can hit tab to fold and unfold full output. Such a simple feature, but such a pain to implement in a UNIX shell/terminal! What follows is an unstructured bag of things learned: I originally tried to use code normally, by iteratively prompting in the terminal until I get the output I want. This was frustrating, as it was too easy to miss a good place to commit a chunk of work, or to rein in a conversation going astray. This “prompting-then-waiting” mode also had a pattern of mental context switches not matching my preferred style of work. This article suggests a better workflow: https://harper.blog/2025/05/08/basic-claude-code/ Instead of writing your single prompt in the terminal, you write an entire course of action as a task list in document, and the actual prompt is then something along the lines of Read @plan.md, complete the next task, and mark it with . After finishes iterating on a step you look at the diff and interactively prompt for necessary corrections. When you are happy, and the conversation, to start the next step from the clean slate. The plan pattern reduces context switches, because it allows you to plan several steps ahead, while you are in the planning mode, even if it makes sense to do the work one step at a time. I often also work on continuing the plan when is working on the current task. A brilliant metaphor from another post https://crawshaw.io/blog/programming-with-agents is that prompting LLM for some coding task and then expecting it to one-shot a working solution is quite a bit like asking a candidate to whiteboard an algorithm during the interview. LLMs are clearly superhuman at whiteboarding, but you can’t go far without feedback. “Agentic” programming like allows LLMs to iterate on solution. LLMs are much better at whiteboarding than at iterating. My experience is that, starting with suboptimal solution, LLM generally can’t improve it by itself along the fuzzy aesthetic metrics I care about. They can make valid changes, but the overall quality stays roughly the same. However, LLMs are tenacious, and can do a lot of iterations. If you do have a value function, you can use it to extract useful work from random walk! A bad value function is human judgement. Sitting in the loop with LLM and pointing out mistakes is both frustrating and slow (you are the bottleneck). In contrast “make this test green” is very efficient at getting working (≠ good) code. LLMs are good at “closing the loop”, they can make the ends meet. This insight combined with the pattern gives my current workflow — spec ↔ code ↔ test loop. Here’s the story: I coded the first version of using just the pattern, but at some point I hit complexity wall. I realized that my original implementation strategy for syntax highlighting was a dead end, and I needed to change it, but that was hard to do without making a complete mess of the code. The accumulated reflected a bunch of historical detours, and the tests were too brittle and coupled to the existing implementation (more on tests later). This worked for incremental additions, but now I wanted to change something in the middle. I realized that what I want is not an append-only that reflects history, but rather a mutable that describes clearly how the software should behave. For normal engineering, this would have been “damn, I guess I need to throw one out and start afresh” moment. With , I added and all the code to the context and asked it to write file in the same task list format. There are two insights here: First , mutable spec is a good way to instruct LLM. When I want to apply a change to now, I prompt to update the spec first (unchecking any items that need re-doing), manually review/touch-up the spec, and use a canned prompt to align the code and tests with the spec. Second , that you can think of an LLM as a machine translation, which can automatically convert between working code, specification, and tests. You can treat any of those things as an input, as if you are coding in miniKanren ! I did have this idea of closing the loop when I started with , so I crafted the prompts to emphasize testing. You can guess the result! wrote a lot of tests, following all the modern “best practices” — a deluge of unit tests that were just needlessly nailing down internal API, a jungle of bug-hiding mocks, and a bunch of unfocused integration tests which were slow, flaky, and contained a copious amount of sleeps to paper over synchronization bugs. Really, this was eerily similar to a typical test suite you can find in the wild. I am wondering why is that? This is perhaps my main take away: if I am vibe-coding anything again, and I want to maintain it and not just one-shot it, I will think very hard about the testing strategy. Really, to tout my own horn, I think that perhaps How to Test? is the best article out there about agentic coding. Test iteration is a multiplier for humans, but a hard requirement for LLMs. Test must be very fast, non-flaky, and should end-to-end test application features , rather than code. Concretely, I just completely wiped out all the existing tests. Then I added testing strategy to the spec. There are two functions: The function waits for all outstanding async work (like external processes) to finish. This requires properly threading causality throughout the code. E.g., there’s a promise you can on to join currently running process. The function captures the entire state of the extension as a single string. There’s just one mock for the clock (another improvement on the usual terminal — process runtime is always show). Then, I prompted with something along the lines of Oups, looks like someone wiped out all the tests here, but the code and the spec look decent, could you re-create the test suite using function as per @spec.md? It worked. Again, “throw one away” is very cheap. That’s it! LLMs obviously can code. You need to hold them right. In particular, you need to engineer a feedback loop to let LLM iterate at its own pace. You don’t want human in the “data plane” of the loop, only in the control plane. Learn to architecture for testing . LLM drastically reduce the activation energy for writing custom tools. I wanted something like forever, but it was never the most attractive yak to shave. Well, now I have the thing, I use it daily. LLMs don’t magically solve all software engineering problems. The biggest time sink with was solving the problem, but LLMs are not yet at the “give me UNIX, but without mess” stage. LLMs don’t solve maintenance. A while ago I wrote about LSP for jj . I think I can actually code that up in a day with Claude now? Not the proof of concept, the production version with everything I would need. But I don’t want to maintain that. I don’t want to context switch to fix a minor bug, if I am the only one using the tool. And, well, if I make this for other people, I’d definitely be on the hook for maintaining it :D

0 views
matklad 1 months ago

Ads Are a Positional Good

Non-technical armchair economics post, where I explain my pet theory for why everything on and outside of the internet is absolutely infested with ads. The traditional explanation is that any paid service will lose out to a “free” service funded by advertising. I think there’s some truth to it! Another explanation is transaction overhead. You basically can’t pay 1 cent for something, as just the annoyance of using the debit card is greater than the value. Contrast with the ease of dismissing a cookie consent banner! I think this is also important, but, at best, a part of the story. The simple fact is that even paying for things doesn’t rid you of those pesky attention vampires! When I go to the cinema, I buy the ticket, but still have to sit through ten minutes of advertisement. Similarly, I pay for subscription to economist.com, and I still see a lot of ads if I disable my ad-blocker. I think there’s a more fundamental force in play here, which is, you guessed it: ads are a positional good. Let me unpack! Positional Good is a term from economics. To explain it, let’s start with a normal good, like bread. You want bread because you derive value from it, you avoid starvation. So you are willing to buy it, and the price you are willing to pay is proportional to the value you personally derive from it. In general, you want to pay only so much for bread, because your hunger is finite. In contrast, the value of a positional good is indirect. It is how much less of the good the others have. What matters is not the absolute amount of good you have, but your relative position among other actors. A standard (though somewhat muddled) example is a University degree. There’s certain intrinsic value in having a degree from a more prestigious University, even if we keep the actual level of knowledge attained fixed. “Better” degree positions you higher relative to other job applicants. So prospective students compete for a limited number of seats not so much with the underlying hard granite of science, but rather with each other, and only the brightest go to the best University (which creates a feedback loop, but this is the other mechanism, not covered by the article). I think it is the same for ads! If you are in a supermarket and want to buy a soft drink, your decision (simplifying, of course!) is based on how much craving you have and how much do you value your craving in euros. If you are particularly thirsty, you might even buy a larger bottle, but unlikely more than one. Now imagine that you are a soft drink company, and you are considering an ad slot before a movie. How much should you pay? Well, it’s easy — just a little bit more than your rival company! Your ads aren’t going to meaningfully affect the amount of thirst people have, but they certainly can nudge the decision of which soft drink to buy. And, given that the other company is locked into the same logic, you are going to spend basically as much as you can afford. And that is my mental model. While advertising creates some value directly, by informing customers of their options, I think that the bulk of ads is pure competitive value destruction, which redirects surplus value created by productive economic activity to companies hosting advertisement battlegrounds, with human attention being merely a collateral damage.

0 views
matklad 1 months ago

Links

If you have a blog, consider adding a “links” page to it, which references resources that you find notable: https://matklad.github.io/links.html I’ve started my links page several years ago, mostly because I found myself referring to the same few links repeatedly in various discussions, and not all the links were easily searchable. Note that the suggestion is different from more typical “monthly links roundup”, which is nice to maintain Substack engagement/community, but doesn’t contribute to long-term knowledge distilling. It is also different from the exhaustive list of everything I’ve read on the Internet. It is relatively short, considering its age.

0 views
matklad 1 months ago

Retry Loop Retry

Some time ago I lamented that I don’t know how to write a retry loop such that: https://matklad.github.io/2023/12/21/retry-loop.html To recap, we have and we need to write I’ve received many suggestions, and the best one was from https://www.joachimschipper.nl , though it was somewhat specific to Python: A couple of days ago I learned to think better about the problem. You see, the first requirement, that the number of retries is bounded syntactically, was leading me down the wrong path. If we start with that requirement, we get code shape like: The salient point here is that, no matter what we do, we need to get or out as a result, so we’ll have to call at least once. But could be zero. Looking at the static semantics, any non loop’s body can be skipped completely, so we’ll have to have some runtime asserts explaining to the compiler that we really did run at least once. The part of the loop which is guaranteed to be executed at least once is a condition. So it’s more fruitful to flip this around: it’s not that we are looping until we are out of attempts, but, rather, we are looping while the underlying action returns an error, and then retries are an extra condition to exit the loop early: This shape of the loop also works if the condition for retries is not attempts based, but, say, time based. Sadly, this throws “loop is obviously bounded” requirement out of the window. But it can be restored by adding upper bound to the infinite loop: I still don’t like it (if you forget that , you’ll get a panic!), but that’s where I am at!

0 views
matklad 2 months ago

Reserve First

A short post about a coding pattern that is relevant for people who use the heap liberally and manage memory with their own hands. Let’s start with two bugs. The first one is from Andrew Kelley’s HYTRADBOI 2025 talk, “Programming Without Pointers” : The second one is from the Ghostty terminal emulator: Can you spot the two bugs? In lieu of a spoiler, allow me to waste your bandwidth with a Dante Gabriel Rossetti painting: In both functions, a bug happens when the second expression throws. In the case, we insert an item into a hash table, but leave it uninitialized. Accessing the item later will crash in the best case. The Ghostty example is even more interesting. It actually tries to avoid this exact problem, by attempting to carefully revert changes in the block. But it fails to do so properly! While the data is restored to on error, the still frees , so we end up with uninitialized memory all the same: Both are “exception safety” problems: if we attempt an operation that mutates an object, and an error happens midway, there are three possible outcomes: The object state remains as if we didn’t attempt the operation. The object is left in a different, but valid state. The object becomes invalid and unsafe to use. In these two cases in particular, the only source of errors is fallible allocation. And there’s a pattern to fix it: As a reminder , is a Zig idiom for expressing “no errors after this point”. Applying the pattern to two examples we get: Memory reservation is a magic trick, contains all the failures, but doesn’t change the data structure! Do you see how powerful that is? I learned this pattern from Andrew Kelley during the coffee break after the talk! I haven’t measured the optimal level of spice here to make the truest possible statement. Instead I opted for dumping as much spice as possible to get the brain gears grinding: Zig should remove and rename to just . If you want to insert a single item, that’s two lines now. Don’t insert items one-by-one, reserve memory in bulk, up-front. Zig applications should consider aborting on OOM. While the design goal of handling OOM errors correctly is laudable, and Zig makes it possible, I’ve seen only one application, xit which passes “matklad spends 30 minutes grepping for ” test. For libraries, prefer leaving allocation to the caller, or use generative testing with an allocator that actually returns errors. Alternatively, do as TigerBeetle. We take this pattern literally, reserve all resources in main, and never allocate memory afterwards: ARCHITECTURE.md#static-memory-allocation

0 views
matklad 2 months ago

Zig's Lovely Syntax

It’s a bit of a silly post, because syntax is the least interesting detail about the language, but, still, I can’t stop thinking how Zig gets this detail just right for the class of curly-braced languages, and, well, now you’ll have to think about that too. On the first glance, Zig looks almost exactly like Rust, because Zig borrows from Rust liberally. And I think that Rust has great syntax, considering all the semantics it needs to express (see “Rust’s Ugly Syntax” ). But Zig improves on that, mostly by leveraging simpler language semantics, but also through some purely syntactical tasteful decisions. How do you spell a number ninety-two? Easy, . But what type is that? Statically-typed languages often come with several flavors of integers: , , . And there’s often a syntax for literals of a particular types: , , . Zig doesn’t have suffixes, because, in Zig, all integer literals have the same type: : The value of an integer literal is known at compile time and is coerced to a specific type on assignment or ascription: To emphasize, this is not type inference, this is implicit comptime coercion. This does mean that code like generally doesn’t work, and requires an explicit type. Raw or multiline strings are spelled like this: This syntax doesn’t require a special form for escaping itself: It nicely dodges indentation problems that plague every other language with a similar feature. And, the best thing ever: lexically, each line is a separate token. As Zig has only line-comments, this means that is always whitespace. Unlike most other languages, Zig can be correctly lexed in a line-by-line manner. Raw strings is perhaps the biggest improvement of Zig over Rust. Rust brute-forces the problem with syntax, which does the required job, technically, but suffers from the mentioned problems: indentation is messy, nesting quotes requires adjusting hashes, unclosed raw literal breaks the following lexical structure completely, and rustfmt’s formatting of raw strings tends to be rather ugly. On the plus side, this syntax at least cannot be expressed by a context-free grammar! For the record, Zig takes C syntax (not that C would notice): The feels weird! It will make sense by the end of the post. Here, I want only to note part, which matches the assignment syntax . This is great! This means that grepping for gives you all instances where a field is written to. This is hugely valuable: most of usages are reads, but, to understand the flow of data, you only need to consider writes. Ability to mechanically partition the entire set of usages into majority of boring reads and a few interesting writes does wonders for code comprehension. Where Zig departs from C the most is the syntax for types. C uses a needlessly confusing spiral rule. In Zig, all types are prefix: While pointer type is prefix, pointer dereference is postfix, which is a more natural subject-verb order to read: Zig has general syntax for “raw” identifiers: It is useful to avoid collisions with keywords, or for exporting a symbol whose name is otherwise not a valid Zig identifier. It is a bit more to type than Kotlin’s delightful , but manages to re-use Zig’s syntax for built-ins ( ) and strings. Like, Rust, Zig goes for function declaration syntax. This is such a massive improvement over C/Java style function declarations: it puts token (which is completely absent in traditional C family) and function name next to each other, which means that textual search for allows you to quickly find the function. Then Zig adds a little twist. While in Rust we write The arrow is gone! Now that I’ve used this for some time, I find arrow very annoying to type, and adding to the visual noise. Rust needs the arrow: Rust has lambdas with an inferred return type, and, in a lambda, the return type is optional. So you need some sort of an explicit syntax to tell the parser if there is return type: And it’s understandable that lambdas and functions would want to use compatible syntax. But Zig doesn’t have lambdas, so it just makes the type mandatory. So the main is Related small thing, but, as name of the type, I think I like more than . Zig is using and for binding values to names: This is ok, a bit weird after Rust’s, whose would be in Zig, but not really noticeable after some months. I do think this particular part is not great, because , the more frequent one, is longer. I think Kotlin nails it: , , . Note all three are monosyllable, unlike and ! Number of syllables matters more than the number of letters! Like Rust, Zig uses syntax for ascribing types, which is better than because optional suffixes are easier to parse visually and mechanically than optional prefixes. Zig doesn’t use and and spells the relevant operators as and : This is easier to type and much easier to read, but there’s also a deeper reason why they are not sigils. Zig marks any control flow with a keyword. And, because boolean operators short-circuit, they are control flow! Treating them as normal binary operator leads to an entirely incorrect mental model. For bitwise operations, Zig of course uses and . Both Zig and Rust have statements and expressions. Zig is a bit more statement oriented, and requires explicit returns: Furthermore, because there are no lambdas, scope of return is always clear. Relatedly, the value of a block expression is void. A block is a list of statements, and doesn’t have an optional expression at the end. This removes the semicolon problem — while Rust rules around semicolons are sufficiently clear (until you get to macros), there’s some constant mental overhead to getting them right all the time. Zig is more uniform and mechanical here. If you need a block that yields a value, Zig supports a general syntax for breaking out of a labeled block: Rust makes pedantically correct choice regarding s: braces are mandatory: This removes the dreaded “dangling else” grammatical ambiguity. While theoretically nice, it makes -expression one-line feel too heavy. It’s not the braces, it’s the whitespace around them: But the ternary is important! Exploding a simple choice into multi-line condition hurts readability. Zig goes with the traditional choice of making parentheses required and braces optional: By itself, this does create a risk of style bugs. But in Zig formatter (non-configurable, user-directed) is a part of the compiler, and formatting errors that can mask bugs are caught during compilation. For example, is an error due to inconsistent whitespace around the minus sign, which signals a plausible mixup of infix and binary minus. No such errors are currently produced for incorrect indentation (the value add there is relatively little, given ), but this is planned. NB: because Rust requires branches to be blocks, it is forced to make synonym with . Otherwise, the ternary would be even more unusable! Syntax design is tricky! Whether you need s and whether you make or mandatory in ifs are not orthogonal! Like Python, Zig allows on loops. Unlike Python, loops are expressions, which leads to a nicely readable imperative searches: Zig doesn’t have syntactically-infinite loop like Rust’s or Go’s . Normally I’d consider that a drawback, because these loops produce different control flow, affecting reachability analysis in the compiler, and I don’t think it’s great to make reachability dependent on condition being visibly constant. But! As Zig places semantics front and center, and the rules for what is and isn’t a comptime constant are a backbone of every feature, “anything equivalent to ” becomes sufficiently precise. Incidentally, these days I tend to write “infinite” loops as Almost always there is an up-front bound for the number of iterations until the break, and its worth asserting this bound, because debugging crashes is easier than debugging hangs. , , , , and all use the same Ruby/Rust inspired syntax for naming captured values: I like how the iterator comes first, and then the name of an item follows, logically and syntactically. I have a very strong opinion about variable shadowing. It goes both ways: I spent hours debugging code which incorrectly tried to use a variable that was shadowed by something else, but I also spent hours debugging code that accidentally used a variable that should have been shadowed! I really don’t know whether on balance it is better to forbid or encourage shadowing! Zig of course forbids shadowing, but what’s curious is that it’s just one episode of the large crusade against any complexity in name resolution. There’s no “prelude”, if you want to use anything from std, you need to import it: There are no glob imports, if you want to use an item from std, you need to import it: Zig doesn’t have inheritance, mixins, argument-dependent lookup, extension functions, implicit or traits, so, if you see , that is guaranteed to be a boring method declared on type. Similarly, while Zig has powerful comptime capabilities, it intentionally disallows declaring methods at compile time. Like Rust, Zig used to allow a method and a field to share a name, because it actually is syntactically clear enough at the call site which is which. But then this feature got removed from Zig. More generally, Zig doesn’t have namespaces. There can be only one kind of in scope, while Rust allows things like I am astonished at the relative lack of inconvenience in Zig’s approach. Turns out that is all the syntax you’ll ever need for accessing things? For the historically inclined, see “The module naming situation” thread in the rust mailing list archive to learn the story of how rust got its syntax. The lack of namespaces touches on the most notable (by its absence) feature of Zig syntax, which deeply relates to the most profound aspect of Zig’s semantics. Everything is an expression. By which I mean, there’s no separate syntactic categories of values, types, and patterns. Values, types, and patterns are of course different things. And usually in the language grammar it is syntactically obvious whether a particular text fragment refers to a type or a value: So the standard way is to have separate syntax families for the three categories, which need to be internally unambiguous, but can be ambiguous across the categories because the place in the grammar dictates the category: when parsing , everything until is a pattern, stuff between and is a type, and after we have a value. There are two problems here. First, there’s a combinatorial explosion of sorts in the syntax, because, while three categories describe different things, it turns out that they have the same general tree-ish shape. The second problem is that it might be hard to maintain category separation in the grammar. Rust started with the three categories separated by a bright line. But then, changes happen. Originally, Rust only allowed syntax for assignment. But today you can also write to do unpacking like Similarly, the turbofish used to move the parser from the value to the type mode, but now const parameters are values that can be found in the type position! The alternative is not to pick this fight at all. Rather than trying to keep the categories separately in the syntax, use the same surface syntax to express all three, and categorize later, during semantic analysis. In fact, this is already happens in the example — these are different things! One is a place (lvalue) and another is a “true” value (rvalue), but we use the same syntax for both. I don’t think such syntactic unification necessarily implies semantic unification, but Zig does treat everything uniformly, as a value with comptime and runtime behavior (for some values, runtime behavior may be missing, for others — comptime): The fact that you can write an where a type goes is occasionally useful. But the fact that simple types look like simple values syntactically consistently make the language feel significantly less busy. As a special case of everything being an expression, instances of generic types look like this: Just a function call! Though, there’s some resistance to trickery involved to make this work. Usually, languages rely on type inference to allow eliding generic arguments. That in turn requires making argument syntax optional, and that in turn leads to separating generic and non-generic arguments into separate parameter lists and some introducer sigil for generics, like or . Zig solves this syntactic challenge in the most brute-force way possible. Generic parameters are never inferred, if a function takes 3 comptime arguments and 2 runtime arguments, it will always be called with 5 arguments syntactically. Like with the (absence of) importing flourishes, a reasonable reaction would be “wait, does this mean that I’ll have to specify the types all the time?” And, like with import, in practice this is a non-issue. The trick are comptime closures. Consider a generic : We have to specify type when creating an instance of an . But subsequently, when we are using the array list, we don’t have to specify the type parameter again, because the type of variable already closes over . This is the major truth of object-orienting programming, the truth so profound that no one even notices it: in real code, 90% of functions are happiest as (non-virtual) methods. And, because of that, the annotation burden in real-world Zig programs is low. While Zig doesn’t have Hindley-Milner constraint-based type inference, it relies heavily on one specific way to propagate types. Let’s revisit the first example: This doesn’t compile: and are different values, we can’t select between two at runtime because they are different. We need to coerce the constants to a specific runtime type: But this doesn’t kick the can sufficiently far enough and essentially reproduces the with two incompatible branches. We need to sink coercion down the branches: And that’s exactly how Zig’s “Result Location Semantics” works. Type “inference” runs a simple left-to-right tree-walking algorithm, which resembles interpreter’s . In fact, is exactly what happens. Zig is not a compiler, it is an interpreter. When evaluates an expression, it gets: When interpreting code like the interpreter passes the result location ( ) and type down the tree of subexpressions. If branches store result directly into object field (there’s a inside each branch, as opposed to one after the ), and each coerces its comptime constant to the appropriate runtime type of the result. This mechanism enables concise syntax for specifying enums: When evaluates the switch, it first evaluates the scrutinee, and realizes that it has type . When evaluating arm, it sets result type to for the condition, and a literal gets coerced to . The same happens for the second arm, where result type further sinks down the . Result type semantics also explains the leading dot in the record literal syntax: Syntactically, we just want to disambiguate records from blocks. But, semantically, we want to coerce the literal to whatever type we want to get out of this expression. In Zig, is a shorthand for . I must confess that did weird me out a lot at first during writing code (I don’t mind reading the dot). It’s not the easiest thing to type! But that was fixed once I added snippet, expanding to . The benefits to lightweight record literal syntax are huge, as they allow for some pretty nice APIs. In particular, you get named and default arguments for free: I don’t really miss the absence of named arguments in Rust, you can always design APIs without them. But they are free in Zig, so I use them liberally. Syntax wise, we get two features (calling functions and initializing objects) for the price of one! Finally, the thing that weirds out some people when they see Zig code, and makes others reconsider their choice GitHub handles, even when they haven’t seen any Zig: syntax for built-in functions. Every language needs to glue “userspace” code with primitive operations supported by the compiler. Usually, the gluing is achieved by making the standard library privileged and allowing it to define intrinsic functions without bodies, or by adding ad-hoc operators directly to the language (like Rust’s ). And Zig does have a fair amount of operators, like or . But the release valve for a lot of functionality are built-in functions in distinct syntactic namespace, so Zig separates out , , , , , , , , , and . There’s no need to overload casting when you can give each variant a name. There’s also for type ascription. The types goes first, because the mechanism here is result type semantics: evaluates the first argument as a type, and then uses that as the type for the second argument. Curiously, I think actually can be implemented in the userspace: In Zig, a type of function parameter may depend on values of preceding (comptime) ones! My favorite builtin is . First, it’s the most obvious way to import code: Its crystal clear where the file comes from. But, second, it is an instance of reverse syntax sugar. You see, import isn’t really a function. You can’t do The argument of has to be a string, syntactically. It really is syntax, except that the function-call form is re-used, because it already has the right shape. So, this is it. Just a bunch of silly syntactical decisions, which add up to a language which is positively enjoyable to read. As for big lessons, obviously, the less features your language has, the less syntax you’ll need. And less syntax is generally good, because varied syntactic constructs tend to step on each other toes. Languages are not combinations of orthogonal aspects. Features tug and pull the language in different directions and their combinations might turn to be miraculous features in their own right, or might drag the language down. Even with a small feature-set fixed, there’s still a lot of work to pick a good concrete syntax: unambiguous to parse, useful to grep, easy to read and not to painful to write. A smart thing is of course to steal and borrow solutions from other languages, not because of familiarity, but because the ruthless natural selection tends to weed out poor ideas. But there’s a lot of inertia in languages, so there’s no need to fear innovation. If an odd-looking syntax is actually good, people will take to it. Is there anything about Zig’s syntax I don’t like? I thought no, when starting this post. But in the process of writing it I did discover one form that annoys me. It is the while with the increment loop: This is two-thirds of a C-style loop (without the declarator), and it sucks for the same reason: control flow jumps all over the place and is unrelated to the source code order. We go from condition, to the body, to the increment. But in the source order the increment is between the condition and the body. In Zig, this loop sucks for one additional reason: that separating the increment I think is the single example of control flow in Zig that is expressed by a sigil, rather than a keyword. This form used to be rather important, as Zig lacked a counting loop. It has form now, so I am tempted to call the while-with-increment redundant. Annoyingly, is almost equivalent to But not exactly: if contains a , or , the version would run the one extra time, which is useless and might be outright buggy. Oh well.

0 views
matklad 2 months ago

Partially Matching Zig Enums

Usually, you handle it like this: But once in a while, there’s common handling code you want to run for several variants. The most straightforward way is to duplicate: But this gets awkward if common parts are not easily extractable into function. The “proper” way to do this is to refactor the enum: This gets very awkward if there’s one hundred usages of , 95 of them look better with flat structure, one needs common code for ab case, and the four remaining need common code for ac. The universal recipe for solving the AB problem relies on a runtime panic: And… this is fine, really! I wrote code of this shape many times, and it never failed at runtime due to a misapplied refactor later. Still, every time I write that , I die inside a little. Surely there should be some way to explain to the compiler that is really unreachable there? Well, as I realized an hour ago, in Zig, you can! This is the awkward runtime-panicky and theoretically brittle version: And here’s a bullet-proof compiler-checked one: There are two tricks here. forces the compiler to generate the program twice, where is bound to comptime value. The second trick is , which instructs the compiler to fail if it gets to the else branch. But, because is known at comptime, compiler knows that is in fact unreachable, and doesn’t hit the error. Adding a bug fails compilation, as intended:

0 views
matklad 3 months ago

font-size-adjust Is Useful

In this article, I will describe a recent addition to CSS, the property. I am also making a bold claim that everyone in the world misunderstands the usefulness of this property, including Google , MDN , and CSS Specification itself . (Just to clarify, no, I am not a web designer and I have no idea what I am talking about). Let’s start with oversimplified and incorrect explanation of (see https://tonsky.me/blog/font-size/ for details). Let’s say you specified . What does that mean? First, draw a square 96 pixels high: Then, draw a letter “m” somewhere inside this box: This doesn’t make sense? I haven’t told you how large the letter m should be? Tiny? Huge? Well, sorry, but that’s really how font size works. It’s a size of the box around the glyph, not the size of the glyph. And there isn’t really much consistency between the fonts as to how large the glyph itself should be. Here’s a small “x” in the three fonts used on my blog at 48px font size: They are quite different! And this is where comes in. If I specify , I ask the browser to scale the font such that the letter “x” is exactly half of the box. This makes the fonts comparable: Now, the part where I foolishly disagree with the world! The way this property is described in MDN and elsewhere is as if it only matters for the font fallback. That is, if you have , one potential problem could be that the fallback sans-serif font on the user’s machine will have very different size from Futura. So, the page could look very differently depending on whether fallback kicks in or not (and fallback can kick in temporarily , while the font is being loaded). So, the official guideline is, roughly, When using font fallback, find a value of that makes no change for the first font of the fallback stack. I don’t find this to be a particularly compelling use-case! Make sure to vendor the fonts used, specify inline in a tag inside the to avoid extra round trips, add and FOUC is solved for most people. Otherwise, you might want to stick to font. A use-case for I find much more compelling is that you probably are going to use several fonts on a web-page. And you also might change fonts in the future. And they will have different intrinsic size because that’s how the things are. Part of the mess is avoidable by pinning the meaning of font size. So, the guideline I’d use is: Stick into your CSS reset, right next to . Why ? That’s the invariant ratio for Helvetica, but any number in that vicinity should work! While fixes the size of the glyph relative to em-square, it doesn’t fix position of the glyph. This can create line height problems. Consider these two paragraphs that are styled with , but where the second paragraph is using monospace font for : You are supposed to use coreutils to solve this problem. You are supposed to use coreutils to solve this problem. You are supposed to use coreutils to solve this problem. You are supposed to use coreutils to solve this problem. You are supposed to use to solve this problem. You are supposed to use to solve this problem. You are supposed to use to solve this problem. You are supposed to use to solve this problem. In the first paragraph, each line is indeed 24 pixels high, but in the second paragraph each line is slightly larger, despite the line-height being set to 24px explicitly. How can this be? The full answer is in: https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align The TL;DR is that doesn’t actually set the the height of the line (who would have thought!). Instead, it sets the height of each individual span of the text on the line. So, both “supposed” and “ ” have a 24 pixels high box around them. But because relative position of glyphs inside the em-box is different between the two fonts, the boxes are shifted relative to each other to align the baselines. You can see that by adding : You are supposed to use to solve this problem. You are supposed to use to solve this problem. You are supposed to use to solve this problem. You are supposed to use to solve this problem. If we align the boxes, than baselines are not aligned. It follows that when we align the baselines, the boxes are misaligned, and the line-height ends up larger than the height of any box, because boxes stick out! I don’t know a great solution here. A hack is to say something like such that text set in monospace font doesn’t affect line height calculation. Counter-intuitively, this will work even if the line is entirely monospace (see in the abovelinked article).

0 views
matklad 3 months ago

Inverse Triangle Inequality

| A C | < | A B | + | B C | is one of the most hardworking inequalities in mathematics. It illustrates an obvious fact — the fastest way to go from point A to point C is to go directly, without detours: This is a tremendously useful inequality, not because most of the mathematics lives on the Euclidean plane, but because it works in many different contexts. For example, you could measure distance the Manhattan way , as a sum of vertical and horizontal displacements, and the inequality still holds. Or you could measure Levenshtein distance between DNA strings, and observe the same inequality. The inequality is definitional — if you have a notion of distance between some kinds of objects that obeys triangle inequality (and some other obvious properties), you immediately get a metric space with a bunch of useful properties. I like to think about software engineering as a mischievous younger sibling of mathematics, who respects all the hard work that went into theorems, but doesn’t hesitate to give them a more light-hearted and silly touch. In programming, I find myself referring again and again to the idea of inverse of triangle inequality, | A C | > | A B | + | B C | . If you need to go from A to C, it is almost always easier to reach midpoint B first! Split large pull requests into smaller ones. Land simple, uncontroversial cleanups fast, and leave only meaningful changes up for discussion. Don’t let work already done linger behind uncertain design decisions. There are many sub-genres here. Often, you notice an unrelated cleanup, file it right away as a separate PR. Sometimes, the cleanup is very much related and required to enable what you set out to do, but it can also stand on its own. If it can, it should! In many cases, “the thing” itself can be sliced up. When upgrading to a newer language version recently, we significantly reduced the diff by splitting out the changes that worked under both the new and the old version into a series of preparation pull requests. If you follow obvious development practices (no flaky tests, not rocket science rule , programmers prioritize reviewing code over writing more code), such PR chunking significantly reduces the time to land the aggregate work in: small increments land with the latency of the CI, because the review is trivial, and bigger increments land faster because the discussion is focused on the meaningful changes to the behavior of the software, and not on the large diff itself. Note, however, that the “size” of commit/pull-request is only a proxy! The actual advice is to find independent work. While it is true that bigger changes usually tend to be composed of several independent, smaller changes, that is not always true. Most of my commits are tiny-tiny “fix punctuation in a comment”, “clarify variable name”, etc, but occasionally I do a massive change in one go, if it can’t be reasonably split. On a smaller scale, often when changing this and that, you can change this, and then change that. One common pattern is that doing something requires threading some sort of parameter through a body of code. Thread the parameter first, without actually doing the thing. Once the parameter is there, apply change to the logic. That way, you split a massive diff that changes the logic into a massive diff that just mechanically threads stuff, and a small diff that changes logic. This merits emphasizing, so let me repeat. There are two metrics to a code diff: number of lines changed, and the trickiness of logic. Many, many diffs change a lot of lines, and also contain tricky logic, but the tricky logic is only small part of affected lines. It is well-worth trying to split such a diff into two, one that just mindlessly applies a simple transformation to a large body of code, and the other that has all the smarts in a single file. As a specific example here, when refactoring a popular API, I like to put the focused change to the API itself and the shotgun change to all the callers into separate commits. Yes, that means that after the first commit the code doesn’t build. No, I do not care about that because the not rocket science rule of software engineering only guarantees that the sequence of merge commits in the main branch passes all the tests, and such a merge commit is a unit of . Another example (due to John Carmack I believe) is that, when you want to refactor to change something, you should start with duplicating the implementation, then changing the copy, then changing the call-sites to use the new copy, and finally with eliminating the original. This scales down! When I want to change a statement inside a single function, I first copy it, and change my copy. I have original visible just above (or in a split, if that’s a large switch), and having it immediately available makes it much easier to copy-paste old fragments into the new version. Any change can be decomposed into an addition and a deletion, and it’s usually worth it. Releasing software is often a stressful and a time-consuming affair. The solution is simple: do more releases. For software that needs to be delivered to the users, I find that a weekly cadence works best: you prepare a release on Friday, let fuzzers&early adopters kick the tires during the weekend, and promote the release to latest on Monday. The killer feature? Because releases are so frequent, completely skipping a release is not a problem at all. So, if there’s any doubt about code being good on Monday, you just skip one, and then have a whole week to investigate. Because the diff between two releases is small, it’s much easier to assess the riskiness of the release, you reduce the risk of forgetting an important change, and, should the things go south, you massively simplify incident mitigation, because the circle of potential culprits is so narrow. Conversely, on the upgrading side, it’s easier to do a boring, routine upgrade every week, with an occasional manual intervention, than to go through a giant list of breaking changes once a year, trying to figure out what might be breaking silently. Curiously, we have an almost opposite (actually, complementary) principle at TigerBeetle . We generally try to put a bit of extra into code we write. It’s ok if a PR takes a day, a week, or a month longer to land, if we are able to explore the problem space more thoroughly, and solve the issue fully, rather than applying a simplest, smallest change to mitigate it. Spending time to get things exactly right is worth it, because the code is an asset, it will pay for itself many times over during its lifetime. Deep cuts approach mitigates the main risk of incremental, one inverse-triangle-inequality step at a time approach — getting stuck in the local optimum. Going from A to Z one letter at a time might be fun and easy, unless you realize around K that Z is not the place you want to finish at. Undoing partial work is messy. In contrast, spiking the full solution in one go, and then going an extra mile to explore beyond its boundaries gives you way more confidence that the code you are writing is the code you should be writing. Another benefit of always doing one step deeper is that it minimizes context switches. Writing code is very easy. Reading is much harder. Code is a projection of the mental model of software from your head, writing, projecting, is an easy, mechanical task, while reconstructing the model from code is necessarily more involved. If you switched mental gears to think about a problem, you might as well think it through, write down the results, and then never return back! I often combine the two approaches. I do the same work twice. The first cut is an end-to-end solution with some corner-cutting and extremely messy git history. The goal is to explore, to try many approaches and find the one that fits. After I am satisfied with the end goal, I redo the work again, this time as a series of independent, incremental changes and refactors. The second time, I often end up doing things slightly differently, immediate rewrites are much cheaper than after-the-fact rewrites, but still allow you to see the problem under a different angle.

0 views