<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><title>Eli Bendersky's website</title><item><title>Plugins case study: Pluggy</title><link>https://eli.thegreenplace.net/2026/plugins-case-study-pluggy/</link><description>&lt;p&gt;Recently I came upon &lt;a class="reference external" href="https://pluggy.readthedocs.io/en/latest/"&gt;Pluggy&lt;/a&gt;,
a Python library for developing plugin systems. It was originally developed
as part of the &lt;tt class="docutils literal"&gt;pytest&lt;/tt&gt; project - known for its rich plugin ecosystem - and
later extracted into a standalone library. You're supposed to reach out for
Pluggy if you want to add a plugin system …&lt;/p&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="section" id="using-pluggy" morss_own_score="2.848" morss_score="21.059533297529537"&gt;
&lt;h2&gt;Using Pluggy&lt;/h2&gt;
&lt;p&gt;Pluggy is built around the concept of &lt;em&gt;hooks&lt;/em&gt;: functions that host
applications or tools (from here on, just "hosts") expose and plugins implement.
A host exposes hooks by using
a decorator returned from &lt;tt&gt;pluggy.HookspecMarker&lt;/tt&gt; and a plugin implements this
hook using a decorator returned from &lt;tt&gt;pluggy.HookimplMarker&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Pluggy's &lt;a href="https://pluggy.readthedocs.io/en/stable/"&gt;documentation&lt;/a&gt; explains
this fairly well; in this post, I'll show how to implement the &lt;tt&gt;htmlize&lt;/tt&gt; tool
with some plugins, introduced in &lt;a href="https://eli.thegreenplace.net/2012/08/07/fundamental-concepts-of-plugin-infrastructures"&gt;the original article in my plugin series&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As a reminder, &lt;tt&gt;htmlize&lt;/tt&gt; is a toy tool that takes markup notation similar to
reStructuredText, and converts it to to HTML. It supports plugins to handle
custom "roles" like:&lt;/p&gt;
&lt;pre&gt;some text :role:`customized text` and more text
&lt;/pre&gt;
&lt;p&gt;As well as plugins that do arbitrary processing on the entire text.&lt;/p&gt;

&lt;h3&gt;Defining hooks&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/eliben/code-for-blog/tree/main/2026/plugin-pluggy/htmlize/htmlize"&gt;Out host&lt;/a&gt; defines two hooks:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;pluggy&lt;/span&gt;

&lt;span&gt;hookspec&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pluggy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HookspecMarker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"htmlize"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

&lt;span&gt;@hookspec&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;firstresult&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;True&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;htmlize_role_handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;role_name&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;
    &lt;span&gt;"""Return a function accepting role contents.&lt;/span&gt;

&lt;span&gt;    The function will be called with a single argument - the role contents, and&lt;/span&gt;
&lt;span&gt;    should return what the role gets replaced with.&lt;/span&gt;
&lt;span&gt;    """&lt;/span&gt;
    &lt;span&gt;pass&lt;/span&gt;

&lt;span&gt;@hookspec&lt;/span&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;htmlize_contents&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;db&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;
    &lt;span&gt;"""Return a function accepting full document contents.&lt;/span&gt;

&lt;span&gt;    The function will be called with a single argument - the document contents&lt;/span&gt;
&lt;span&gt;    (after paragraph splitting and role processing), and should return the&lt;/span&gt;
&lt;span&gt;    transformed contents.&lt;/span&gt;
&lt;span&gt;    """&lt;/span&gt;
    &lt;span&gt;pass&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;A hook is created by calling &lt;tt&gt;HookspecMarker&lt;/tt&gt; with the project's name. This
project name has to match between the host and its plugins. Pluggy is permissive
about what hooks accept as parameters and what they return; for maximal
flexibility and to stay true to the original &lt;tt&gt;htmlize&lt;/tt&gt; example, our hooks
return functions.&lt;/p&gt;
&lt;p&gt;To accompany this &lt;tt&gt;HookspecMarker&lt;/tt&gt;, the host also defines a &lt;tt&gt;HookimplMarker&lt;/tt&gt; with
the same name:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;hookimpl&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pluggy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HookimplMarker&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"htmlize"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;This is used by plugins to attach to hooks when they're loaded.&lt;/p&gt;

&lt;div class="section" id="loading-plugins-in-the-host" morss_own_score="2.8469387755102042" morss_score="16.483089010251987"&gt;
&lt;h3&gt;Loading plugins in the host&lt;/h3&gt;
&lt;p&gt;The host's main function loads plugins at startup as follows:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;pm&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;pluggy&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PluginManager&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"htmlize"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;pm&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;add_hookspecs&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;hookspecs&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;pm&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;load_setuptools_entrypoints&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;"htmlize"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;&lt;tt&gt;hookspecs&lt;/tt&gt; is our Python module containing the hooks shown above.
&lt;tt&gt;load_setuptools_entrypoints&lt;/tt&gt; is Pluggy's helper for loading plugins that
were &lt;tt&gt;pip&lt;/tt&gt;-installed into the same environment and registered as
setuptools &lt;a href="https://setuptools.pypa.io/en/latest/userguide/entry_point.html"&gt;entry points&lt;/a&gt;.
It's a way to signal - in one's &lt;tt&gt;setup.py&lt;/tt&gt; or &lt;tt&gt;pyproject.toml&lt;/tt&gt; file - some
metadata that projects can review at runtime. In our project, the plugins
register themselves with this section in the &lt;tt&gt;pyproject.toml&lt;/tt&gt; file:&lt;/p&gt;
&lt;pre&gt;[project.entry-points.htmlize]
tt = "tt"
&lt;/pre&gt;
&lt;p&gt;This says "for entry point &lt;tt&gt;htmlize&lt;/tt&gt;, define a new entry named &lt;tt&gt;tt&lt;/tt&gt;".
Pluggy's &lt;tt&gt;load_setuptools_entrypoints&lt;/tt&gt; then uses &lt;a href="https://docs.python.org/3/library/importlib.metadata.html"&gt;importlib.metadata&lt;/a&gt;
to access this information.&lt;/p&gt;
&lt;p&gt;Note that Pluggy doesn't require using this mechanism. Hosts can implement any
plugin discovery method they want, and add plugins directly to their
&lt;tt&gt;PluginManager&lt;/tt&gt; with the &lt;tt&gt;register&lt;/tt&gt; method. But this is the mechanism used
for &lt;tt&gt;pytest&lt;/tt&gt; and many other projects; it makes it very easy to
automatically discover and register plugins that are installed with &lt;tt&gt;pip&lt;/tt&gt; and
equivalent tools.&lt;/p&gt;
&lt;/div&gt;

&lt;h3&gt;Invoking plugins&lt;/h3&gt;
&lt;p&gt;Once &lt;tt&gt;PluginManager&lt;/tt&gt; loads the plugins, invoking them is straightforward;
here's how &lt;tt&gt;htmlize&lt;/tt&gt; invokes the contents hooks &lt;/p&gt;
&lt;pre&gt;&lt;span&gt;# Build full contents back again, and ask plugins to act on&lt;/span&gt;
&lt;span&gt;# contents.&lt;/span&gt;
&lt;span&gt;contents&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;''&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;parts&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;for&lt;/span&gt; &lt;span&gt;handler&lt;/span&gt; &lt;span&gt;in&lt;/span&gt; &lt;span&gt;plugin_manager&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hook&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;htmlize_contents&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;db&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;db&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;
    &lt;span&gt;contents&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;contents&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
&lt;span&gt;return&lt;/span&gt; &lt;span&gt;contents&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;Generally, hook invocations return a &lt;em&gt;list&lt;/em&gt; of all the hooks attached to by
different plugins (a single host application can have multiple plugins installed
and attaching to the same hook). When the host invokes the hook as shown above,
the default order is LIFO, but plugins can affect this with
&lt;a href="https://pluggy.readthedocs.io/en/stable/#call-time-order"&gt;hook options&lt;/a&gt;
like &lt;tt&gt;tryfirst&lt;/tt&gt; and &lt;tt&gt;trylast&lt;/tt&gt;.&lt;/p&gt;


&lt;h3&gt;Implementing hooks in plugins&lt;/h3&gt;
&lt;p&gt;Here's our entire &lt;tt&gt;narcissist&lt;/tt&gt; plugin that's attaching to the contents hook:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;import&lt;/span&gt; &lt;span&gt;htmlize&lt;/span&gt;

&lt;span&gt;@htmlize&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hookimpl&lt;/span&gt;
&lt;span&gt;def&lt;/span&gt; &lt;span&gt;htmlize_contents&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;db&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;
    &lt;span&gt;repl&lt;/span&gt; &lt;span&gt;=&lt;/span&gt; &lt;span&gt;f&lt;/span&gt;&lt;span&gt;'&amp;lt;b&amp;gt;I (&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;author&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&amp;lt;/b&amp;gt;'&lt;/span&gt;

    &lt;span&gt;def&lt;/span&gt; &lt;span&gt;hook&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;contents&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;
        &lt;span&gt;return&lt;/span&gt; &lt;span&gt;re&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;'\bI\b'&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;repl&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;contents&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;

    &lt;span&gt;return&lt;/span&gt; &lt;span&gt;hook&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;Some notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It expects &lt;tt&gt;htmlize&lt;/tt&gt; to be installed; as discussed previously, we rely on
Pluggy's default install-based approach where both the host and plugins are
installed into the same Python environment and can thus find each other.
However, Pluggy supports any custom discovery method.&lt;/li&gt;
&lt;li&gt;It uses the &lt;tt&gt;hookimpl&lt;/tt&gt; exported value shown earlier.&lt;/li&gt;
&lt;li&gt;It returns a function that acts on contents; this is the &lt;tt&gt;htmlize&lt;/tt&gt;-specific
contract (ABI, if you will) we've discussed before.&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
</ns0:encoded><pubDate>Sat, 13 Jun 2026 20:21:00 </pubDate></item><item><title>Thoughts on starting new projects with LLM agents</title><link>https://eli.thegreenplace.net/2026/thoughts-on-starting-new-projects-with-llm-agents/</link><description>&lt;p&gt;A few months ago I wrote about &lt;a class="reference external" href="https://eli.thegreenplace.net/2026/rewriting-pycparser-with-the-help-of-an-llm/"&gt;using LLM agents to help restructuring one of my
Python projects&lt;/a&gt;.
It's worth beginning by saying that the
rewrite has been successful by all reasonable measures; I've been able to
continue maintaining that project since then without an issue.&lt;/p&gt;
&lt;p&gt;In this post, I …&lt;/p&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="entry-content" morss_own_score="5.688984881209503" morss_score="19.516728071619426"&gt;




&lt;p&gt;A few months ago I wrote about &lt;a href="https://eli.thegreenplace.net/2026/rewriting-pycparser-with-the-help-of-an-llm/"&gt;using LLM agents to help restructuring one of my
Python projects&lt;/a&gt;.
It's worth beginning by saying that the
rewrite has been successful by all reasonable measures; I've been able to
continue maintaining that project since then without an issue.&lt;/p&gt;
&lt;p&gt;In this post, I want to discuss another project I've recently completed with
significant help from agents: &lt;a href="https://eli.thegreenplace.net/2026/watgo-a-webassembly-toolkit-for-go/"&gt;watgo&lt;/a&gt;. In
this project many things are different; most notably, it's a from-scratch
project rather than a rewrite, and it uses a different programming language
(Go). This post describes my experience working on the project, and some lessons
learned along the way.&lt;/p&gt;

&lt;h2&gt;The process&lt;/h2&gt;
&lt;p&gt;This is a new project, so it required extensive design. I began by iterating on
the design with the agent, with a sketch of the API. For this purpose, I
recommend using a Markdown file &lt;a href="https://github.com/eliben/watgo/blob/main/doc/notes.md"&gt;committed into the repository&lt;/a&gt;
for future reference.&lt;/p&gt;
&lt;p&gt;After that, I started asking the agent to write CLs &lt;/p&gt;
&lt;p&gt;This point is worth reiterating: sometimes a single CL is a huge step forward,
but requires lots of review, cleanup and refactoring to be viable. I've had
multiple instances where an agent produced several days of work in a single
CL, but I then spent hours instructing it to clean up and refactor. Overall,
it's still a productivity gain, just not as much as some pundits would like us
to believe.&lt;/p&gt;

&lt;div class="section" id="keeping-the-human-in-the-loop" morss_own_score="2.9285714285714284" morss_score="21.166349206349206"&gt;
&lt;h2&gt;Keeping the human in the loop&lt;/h2&gt;
&lt;p&gt;Given the current state of agent capabilities, I think it's worth splitting
projects into two categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Low importance / prototype / throw away projects where deep code
understanding is unnecessary. These can be "vibe-coded" (submitting agent
code without even reviewing it).&lt;/li&gt;
&lt;li&gt;High importance projects that I actually want to maintain; here, vibe-coding
is ill advised and I insist on reviewing and guiding all code the agent
writes before it's submitted (or shortly after, as discussed above).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;tt&gt;watgo&lt;/tt&gt; projects is a clear example of (2): I certainly intend to maintain
this project in the long term, so I insist on code that I understand. With very
few exceptions, no code gets in without full review and often multiple rounds
of revisions.&lt;/p&gt;
&lt;p&gt;Even if the cost for writing code went down, maintaining a project is so much
more than that. It's triaging and fixing bugs, it's thinking through what needs
to be done rather than how to do it, it's keeping the code healthy over time,
and so on. As &lt;a href="https://www.goodreads.com/quotes/273375-everyone-knows-that-debugging-is-twice-as-hard-as-writing"&gt;Brian Kernighan said&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
Everyone knows that debugging is twice as hard as writing a program in the
first place. So if you're as clever as you can be when you write it, how will
you ever debug it?&lt;/blockquote&gt;
&lt;p&gt;Maybe at some point agents will become good enough that projects in category
(2) can be implemented and maintained completely autonomously. Maybe. But
we're certainly not there yet. My hunch is that getting there will require
crossing the AGI line &lt;/p&gt;

&lt;h3&gt;Practical workflow&lt;/h3&gt;
&lt;p&gt;If you're using an agent to send an actual PR and only review &lt;em&gt;that&lt;/em&gt;, it's
difficult to be disciplined enough to actually perform a thorough review. I find
the following method to be more reliable:&lt;/p&gt;
&lt;p&gt;I use a CLI agent running locally in my repository, and ask it to update the
code there. In parallel, I have a VSCode window open in the same project, where
I can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Review the agent's changes using VSCode's diff view&lt;/li&gt;
&lt;li&gt;Make my own tweaks and code changes if needed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once I'm pleased with the change, I manually create a commit.&lt;/p&gt;

&lt;/div&gt;

&lt;h2&gt;Keeping the CLs small&lt;/h2&gt;
&lt;p&gt;As mentioned above, it's imperative to keep making progress in small chunks,
with small enough CLs that a human can fully understand in a single review. It's
very tempting to sprint ahead submitting thousands of lines of code every day,
but this temptation has to be avoided. Coding with an agent is like
speed-reading; yes, you're making more progress, but comprehension suffers
the faster you go.&lt;/p&gt;
&lt;p&gt;Particularly for refactoring, agents still take the shortest route to
destination. It's important to guide them to think about the "big picture" at
all times, find all instances where X is better done as Y, not just a single
place noticed during a review. This is why it's sometimes OK to have
a CL submitted before you fully agree with everything, and go back to it later
for several refactoring rounds. Source control works amazingly well when
pair-coding with agents.&lt;/p&gt;


&lt;h2&gt;Testing strategy&lt;/h2&gt;
&lt;p&gt;It's a key point discussed in every "how to succeed with AI" article, but
still critical enough to reiterate here: a solid testing strategy is absolutely
crucial for success. Agents produce - by far - the best results when they have
a solid test suite to test their code against.&lt;/p&gt;
&lt;p&gt;With the &lt;a href="https://github.com/eliben/pycparser"&gt;pycparser&lt;/a&gt; rewrite, I had
a large existing test suite. For &lt;a href="https://github.com/eliben/watgo"&gt;watgo&lt;/a&gt;,
the very first thing I did was think through how to adapt the test suites of
the &lt;a href="https://github.com/WebAssembly/spec/"&gt;WASM spec&lt;/a&gt; and of the
&lt;a href="https://github.com/WebAssembly/wabt"&gt;wabt project&lt;/a&gt; for my needs.&lt;/p&gt;
&lt;p&gt;If your project doesn't have such tests to rely on, this should be your first
order of business - finding one, or building one from scratch. Beware of
self-reinforcing loops though; it's dangerous to trust agents for both the
tests and the implementations tested against them.&lt;/p&gt;

&lt;div class="section" id="language-choice-go-for-agent-written-projects" morss_own_score="3.0" morss_score="17.0"&gt;
&lt;h2&gt;Language choice - Go for agent-written projects&lt;/h2&gt;
&lt;p&gt;Go is a fantastic language for agents to write, because it's designed to be
very readable by humans. The biggest strengths of Go
are exactly what makes the experience of reviewing agent code so positive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go changes very infrequently, so you don't have to wonder "are we using the
most modern / idiomatic approach" or "what the hell is this construct"
as often as with other languages (looking at you, Python and TypeScript).&lt;/li&gt;
&lt;li&gt;There are relatively few ways to accomplish the same thing in Go, further
lowering the mental burden.&lt;/li&gt;
&lt;li&gt;The standard library is rich and there's much less need to keep abreast of
the package-everyone-uses du jour.&lt;/li&gt;
&lt;li&gt;In general, Go is designed for readability, with a mild-but-still-strong type
system, uniform formatting, explicit error propagation and opinionated choices
already made for you.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since most of the time spent by humans when using agents is &lt;em&gt;reading&lt;/em&gt; rather
than &lt;em&gt;writing&lt;/em&gt; code, these effects compound and produce a great experience.
Recall the discussion of how some languages are optimized for writability (Perl)
while others are optimized for readability (Go)? Well, when working on a project
with an agent we live in a world of 99% reading vs. 1% writing, so this really
matters.&lt;/p&gt;
&lt;p&gt;I find this aspect really crucial in light of the earlier points made in this
post - namely, keeping the human in the loop by understanding and reviewing
all of the agent's design choices and code.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="final-thoughts" morss_own_score="2.895652173913043" morss_score="17.395652173913042"&gt;
&lt;h2&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;If you're working on a subject that's completely new to you, I would strongly
recommend &lt;em&gt;against&lt;/em&gt; the approach described in this post. To really learn
something, you have to work through it from scratch, yourself, reading,
designing, writing the code. Agents don't change this basic fact; even before
agents, if you wanted to learn X, copying it from Stack Overflow or some other
project clearly wasn't the right way to go. Similarly, while agents can be used
as a prop for learning, they cannot learn &lt;em&gt;for you&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;As a corollary, junior engineers should exercise &lt;em&gt;extreme caution&lt;/em&gt; when relying
on LLMs. There's no replacement to hard-won experience and the sweat and tears
of learning new, challenging topics. Learning is supposed to be hard; if it's
too easy, you're probably not learning.&lt;/p&gt;
&lt;p&gt;For senior engineers, agents are a boon; it's a great tool to increase
productivity, avoid the boring stuff, and get unstuck from procrastination; but
only when used judiciously.&lt;/p&gt;
&lt;hr&gt;
&lt;/div&gt;
&lt;/div&gt;
</ns0:encoded><pubDate>Sat, 06 Jun 2026 17:38:00 </pubDate></item><item><title>Notes on Fourier series</title><link>https://eli.thegreenplace.net/2026/notes-on-fourier-series/</link><description>&lt;link rel="stylesheet" href="https://eli.thegreenplace.net/demos/fourier/fourier-plot.css"&gt;&lt;p&gt;The trigonometric Fourier series is a beautiful mathematical theory that
shows how to decompose a periodic function into an infinite sum of
sinusoids. These are my notes on the subject, with some examples and the
connection to linear algebra in Hilbert space.&lt;/p&gt;
&lt;div class="section" id="coefficients-of-fourier-series"&gt;
&lt;h2&gt;Coefficients of Fourier series&lt;/h2&gt;
&lt;p&gt;Let’s assume that …&lt;/p&gt;&lt;/div&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="entry-content" morss_own_score="5.847742696960755" morss_score="31.004749926807598"&gt;




&lt;p&gt;The trigonometric Fourier series is a beautiful mathematical theory that
shows how to decompose a periodic function into an infinite sum of
sinusoids. These are my notes on the subject, with some examples and the
connection to linear algebra in Hilbert space.&lt;/p&gt;
&lt;div class="section" id="coefficients-of-fourier-series" morss_own_score="2.975257731958763" morss_score="41.72071227741331"&gt;
&lt;h2&gt;Coefficients of Fourier series&lt;/h2&gt;
&lt;p&gt;Let’s assume that &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; is a &lt;em&gt;well-behaved&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;Then we say that the &lt;em&gt;Fourier series&lt;/em&gt; on the right-hand side converges
to &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt;. We’ll talk more about the assumptions mentioned above
and convergence in the next section.&lt;/p&gt;
&lt;p&gt;Note that when &lt;img src="https://eli.thegreenplace.net/images/math/4a5997da73aadd118038761e69d01e24586bf958.png"&gt;; therefore
it’s customary to write the series starting with &lt;/p&gt;
&lt;p&gt;Our goal is to find the coefficients &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Integrate both sides of the equation between &lt;/p&gt;
&lt;p&gt;Per Appendix A, all integrals within the sum are zero, so we’re left
with:&lt;/p&gt;
&lt;p&gt;And thus we find &lt;img src="https://eli.thegreenplace.net/images/math/4a5997da73aadd118038761e69d01e24586bf958.png"&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Multiply both sides by &lt;/p&gt;
&lt;p&gt;Looking at the right-hand side, the first integral is zero per Appendix
A, and the last integral is zero per Appendix B. We’re left with:&lt;/p&gt;
&lt;p&gt;Per Appendix B, the integral on the right is zero for all
&lt;/p&gt;
&lt;p&gt;Recall that &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt;; for
consistency, we’ll replace &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt; and isolate
&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Hopefully it’s clear where this is going now; multiply both
sides by &lt;/p&gt;
&lt;p&gt;We’ve just found a way to calculate all the coefficients of our Fourier
series for &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt;:&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Conditions on &lt;em&gt;f&lt;/em&gt; and convergence of Fourier series&lt;/h2&gt;
&lt;p&gt;The previous section discusses Fourier series for a function
&lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; that is &lt;em&gt;well-behaved&lt;/em&gt; - but what does that mean? The full
answer would lead us deep into analysis, which I’d like to avoid here.
So I’ll keep it brief.&lt;/p&gt;
&lt;p&gt;We typically assume that &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; is &lt;a href="https://en.wikipedia.org/wiki/Square-integrable_function"&gt;square
integrable&lt;/a&gt;,
which is denoted as &lt;a href="https://en.wikipedia.org/wiki/Piecewise_function"&gt;piecewise
smooth&lt;/a&gt;: each
segment of the function has continuous derivatives. A very simple
example of a piecewise smooth function is &lt;/p&gt;
&lt;p&gt;These conditions hold for pretty much any reasonable function we want to
approximate using Fourier series, so they aren’t a serious burden.&lt;/p&gt;
&lt;p&gt;For a function &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; that satisfies these conditions, it’s
guaranteed to have a Fourier series that &lt;em&gt;pointwise converges&lt;/em&gt; to it.
This means that at every continuous point of &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt;, the Fourier
series converges to it exactly; at every jump point, the Fourier series
converges to the mid-point of the jump.&lt;/p&gt;


&lt;h2&gt;Cosine and Sine series&lt;/h2&gt;
&lt;p&gt;Sometimes, additional properties of the function &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; can help
us simplify the Fourier series for it. If &lt;a href="https://eli.thegreenplace.net/2025/notes-on-even-and-odd-functions/"&gt;even
function&lt;/a&gt;,
then we know that:&lt;/p&gt;
&lt;p&gt;Because the function inside the integral is odd, and integrating an
odd function over a symmetric interval results in 0.&lt;/p&gt;
&lt;p&gt;Therefore, the Fourier series for such &lt;em&gt;cosine
series&lt;/em&gt;:&lt;/p&gt;
&lt;p&gt;With coefficients &lt;img src="https://eli.thegreenplace.net/images/math/4a5997da73aadd118038761e69d01e24586bf958.png"&gt; and &lt;/p&gt;
&lt;p&gt;Similarly if &lt;em&gt;odd&lt;/em&gt; function, then its &lt;img src="https://eli.thegreenplace.net/images/math/4a5997da73aadd118038761e69d01e24586bf958.png"&gt;
and &lt;em&gt;sine series&lt;/em&gt;:&lt;/p&gt;

&lt;div class="section" id="fourier-series-for-a-non-periodic-function-defined-on-an-interval" morss_own_score="2.9810725552050474" morss_score="28.168572555205046"&gt;
&lt;h2&gt;Fourier series for a non-periodic function defined on an interval&lt;/h2&gt;
&lt;p&gt;So far we’ve been talking about &lt;/p&gt;
&lt;p&gt;E.g. suppose we have &lt;/p&gt;
&lt;p&gt;Yes! First, we have to make a choice of how to extend the function to
the negative interval &lt;em&gt;periodic extension&lt;/em&gt;. Note
that the Fourier series calculation only cares about the range
&lt;/p&gt;
&lt;p&gt;There are several natural ways to extend a function defined on
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Direct periodic repetition: we simply repeat &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; every
&lt;/li&gt;
&lt;li&gt;Even extension: &lt;/li&gt;
&lt;li&gt;Odd extension: &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; when &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s an example of extending our sample function &lt;/p&gt;

&lt;canvas&gt;
  Your browser does not support the HTML5 canvas tag.
  &lt;/canvas&gt;
&lt;p&gt;Note that the Fourier series for these extended functions will be
different. However, they will all converge to &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; in the
interval &lt;/p&gt;
&lt;p&gt;We’ve seen that Fourier series work well for periodic functions and also
non-periodic functions defined on a finite domain (because we can extend
these periodically). But what about aperiodic functions defined on the
entire real line? This is where we’ll have to leave Fourier series
behind and move on to their generalization - the &lt;em&gt;Fourier transform&lt;/em&gt;;
this will be a topic for a separate post.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="example" morss_own_score="2.9731543624161074" morss_score="43.28870991797166"&gt;
&lt;h2&gt;Example&lt;/h2&gt;
&lt;p&gt;Let’s take the following triangular function &lt;/p&gt;

&lt;canvas&gt;
  Your browser does not support the HTML5 canvas tag.
  &lt;/canvas&gt;
&lt;p&gt;Then making an odd extension into &lt;/p&gt;
&lt;p&gt;Since this function is odd, we know that we’ll get a &lt;em&gt;sine series&lt;/em&gt;, as
&lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt;. Let’s calculate
&lt;/p&gt;
&lt;p&gt;Since &lt;/p&gt;
&lt;p&gt;Let’s set &lt;/p&gt;
&lt;p&gt;And split up the integral for the different segments of &lt;/p&gt;
&lt;p&gt;The first integral, by the method described in Appendix C:&lt;/p&gt;
&lt;p&gt;The second integral can also be split into two:&lt;/p&gt;
&lt;p&gt;The first of these is trivial to calculate; the second can once again
use Appendix C. After some tedious but straightforward calculations &lt;/p&gt;
&lt;p&gt;Adding &lt;/p&gt;
&lt;p&gt;Now let’s substitute &lt;/p&gt;
&lt;p&gt;We have &lt;/p&gt;
&lt;p&gt;Note that for even values of &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt;, &lt;/p&gt;
&lt;p&gt;Here’s an interactive chart showing how the series &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt; as for
&lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt; is odd.&lt;/p&gt;

&lt;canvas&gt;
  Your browser does not support the HTML5 canvas tag.
  &lt;/canvas&gt;


&lt;label&gt;
        n (terms in the Fourier series)
        &lt;/label&gt;



&lt;/div&gt;

&lt;h2&gt;Compact formula using a single phase-shifted sinusoid&lt;/h2&gt;
&lt;p&gt;We’ve written the Fourier series for &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; as follows so far:&lt;/p&gt;
&lt;p&gt;We can rewrite this in a somewhat more compact form, using a single
sinusoid with a configurable phase at each &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt;:&lt;/p&gt;
&lt;p&gt;Based on Appendix D, &lt;/p&gt;
&lt;p&gt;When Fourier series are used in the context of signal processing, this
formulation is easier to reason about because it represents the
magnitude and phase shift of each harmonic of &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; in the
frequency domain &lt;/p&gt;

&lt;div class="section" id="complex-fourier-series" morss_own_score="2.9637826961770624" morss_score="38.28246401485838"&gt;
&lt;h2&gt;Complex Fourier series&lt;/h2&gt;
&lt;p&gt;It should not come as a surprise that the Fourier series, being a
combination of trigonometric functions, can also be represented with
complex exponential functions.&lt;/p&gt;
&lt;p&gt;Specifically, we’ll show that our &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; can be approximated as
follows:&lt;/p&gt;
&lt;p&gt;Let’s calculate &lt;/p&gt;
&lt;p&gt;By Appendix A, the sum elements are all zero when &lt;/p&gt;
&lt;p&gt;Therefore, renaming &lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt; (since it’s just an arbitrary
integer constant):&lt;/p&gt;
&lt;p&gt;We’ve found an alternative formulation to Fourier series, using complex
exponentials instead of trigonometric functions. While this was a direct
derivation, another way to achieve the same result is to use the &lt;a href="https://eli.thegreenplace.net/2024/notes-on-the-euler-formula/"&gt;Euler
Formula&lt;/a&gt;
to derive:&lt;/p&gt;
&lt;p&gt;And substitute these into the original Fourier series formula. I’ll
leave this as an exercise for the diligent reader; eventually, the
result will be the same. Moreover, it’s possible to show a direct
correspondence between &lt;/p&gt;
&lt;p&gt;Note that &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt;). This helps
explain why the complex formulation has negative frequencies in the sum;
when the function is actually real, each negative frequency is paired up
with a positive frequency and the result is real &lt;/p&gt;
&lt;p&gt;So, for a real function we only need to account for positive
frequencies:&lt;/p&gt;
&lt;p&gt;We can take it further. &lt;/p&gt;
&lt;p&gt;And substituting back into the sum:&lt;/p&gt;
&lt;p&gt;This is precisely the compact formulation from the previous section!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="fourier-orthogonal-basis-in-hilbert-space" morss_own_score="2.890625" morss_score="34.68305136746143"&gt;
&lt;h2&gt;Fourier orthogonal basis in Hilbert space&lt;/h2&gt;
&lt;p&gt;The most beautiful aspect of Fourier theory is that it doesn’t just
happen to work by chance, and is deeply connected to linear algebra.
Please read &lt;a href="https://eli.thegreenplace.net/2025/hilbert-space-treating-functions-as-vectors/"&gt;my post on Hilbert
space&lt;/a&gt;
before proceeding.&lt;/p&gt;
&lt;p&gt;The space of real-valued square integrable functions &lt;/p&gt;
&lt;p&gt;We’ve demonstrated that the family of functions:&lt;/p&gt;
&lt;p&gt;Are all mutually orthogonal, because their pairwise inner products are
zero! We’ve also shown that any function in &lt;/p&gt;
&lt;p&gt;So these functions form a &lt;em&gt;basis&lt;/em&gt; for &lt;img src="https://eli.thegreenplace.net/images/math/7a38d8cbd20d9932ba948efaa364bb62651d5ad4.png"&gt; in this basis, we usually find the
coefficients by &lt;a href="https://eli.thegreenplace.net/2024/projections-and-projection-matrices/"&gt;projecting
it&lt;/a&gt;
onto the basis. E.g. with a basis vector &lt;img src="https://eli.thegreenplace.net/images/math/7a38d8cbd20d9932ba948efaa364bb62651d5ad4.png"&gt;:&lt;/p&gt;
&lt;p&gt;Similarly, when we calculate the coefficient &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt;, we project &lt;img src="https://eli.thegreenplace.net/images/math/3e03f4706048fbc6c5a252a85d066adf107fcc1f.png"&gt; onto the basis vector
&lt;/p&gt;
&lt;p&gt;From Appendix B, we know that the denominator is &lt;/p&gt;
&lt;p&gt;So we get:&lt;/p&gt;
&lt;p&gt;Which should look familiar!&lt;/p&gt;
&lt;p&gt;This is the core linear-algebra idea behind Fourier series: the
functions &lt;img src="https://eli.thegreenplace.net/images/math/4a0a19218e082a343a1b17e5333409af9d98f0f5.png"&gt; in this
basis. The integral formulas for &lt;/p&gt;
&lt;p&gt;Fourier series therefore let us decompose a function into independent
orthogonal directions, much like decomposing a vector into its
&lt;img src="https://eli.thegreenplace.net/images/math/11f6ad8ec52a2984abaafd7c3b516503785c2072.png"&gt;, &lt;img src="https://eli.thegreenplace.net/images/math/95cb0bfd2977c761298d9624e4b4d4c72a39974a.png"&gt;, and &lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Appendix A: Integrals of sinusoids&lt;/h2&gt;
&lt;p&gt;For any integer &lt;/p&gt;
&lt;p&gt;Similarly:&lt;/p&gt;
&lt;p&gt;Using these, we can calculate the integral of a complex exponential
function for an integer &lt;/p&gt;

&lt;div class="section" id="appendix-b-integrals-of-products-of-sinusoids" morss_own_score="2.880952380952381" morss_score="27.78720238095238"&gt;
&lt;h2&gt;Appendix B: Integrals of products of sinusoids&lt;/h2&gt;
&lt;p&gt;We’ll start with the product of two sines, for any positive integers
&lt;img src="https://eli.thegreenplace.net/images/math/d1854cae891ec7b29161ccaf79a24b00c274bdaa.png"&gt;:&lt;/p&gt;
&lt;p&gt;Using the trigonometric identity for a product of sines, we can write:&lt;/p&gt;
&lt;p&gt;Now let’s focus on two different scenarios, &lt;/p&gt;
&lt;p&gt;If &lt;em&gt;ss&lt;/em&gt; are 0
(see on Appendix A), so &lt;/p&gt;
&lt;p&gt;If &lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;p&gt;We can use exactly the same approach to show that:&lt;/p&gt;
&lt;p&gt;One more variant to cover:&lt;/p&gt;
&lt;p&gt;Since sine is an odd function and cosine is an even function, their
product is an odd function. And the integral of an odd function over a
symmetric interval is 0 (see &lt;a href="https://eli.thegreenplace.net/2025/notes-on-even-and-odd-functions/"&gt;this post for more
details&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Appendix C: A useful integral&lt;/h2&gt;
&lt;p&gt;Let’s calculate the indefinite integral:&lt;/p&gt;
&lt;p&gt;For some constant &lt;/p&gt;
&lt;p&gt;Here &lt;/p&gt;
&lt;p&gt;Putting it together:&lt;/p&gt;

&lt;div class="section" id="appendix-d-sinusoid-with-phase-as-a-sum-of-sin-and-cos" morss_own_score="5.450134770889488" morss_score="33.37870619946092"&gt;
&lt;h2&gt;Appendix D: Sinusoid with phase as a sum of sin and cos&lt;/h2&gt;
&lt;p&gt;Let’s take a general sinusoid with magnitude &lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt; and phase &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt;:&lt;/p&gt;
&lt;p&gt;We’re going to show that &lt;em&gt;sine&lt;/em&gt; and a &lt;em&gt;cosine&lt;/em&gt; with no phase. This is related to &lt;a href="https://eli.thegreenplace.net/2023/sum-of-same-frequency-sinusoids/"&gt;my earlier post
on the sum of same-frequency
sinusoids&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s start by expanding &lt;/p&gt;
&lt;p&gt;Now we’ll denote: &lt;/p&gt;
&lt;p&gt;We have &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt;, but what about the other way around?&lt;/p&gt;
&lt;p&gt;Let’s take the equations:&lt;/p&gt;
&lt;p&gt;Square both of them and add together:&lt;/p&gt;
&lt;p&gt;Now we’ll take the equations for &lt;/p&gt;
&lt;p&gt;Where &lt;a href="https://en.wikipedia.org/wiki/Atan2"&gt;the atan2 function&lt;/a&gt; is
careful to take into account the sign of both numerator and denominator.
Also it’s worth mentioning that &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt; is determined up to
additions of &lt;/p&gt;
&lt;p&gt;To conclude, for any &lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt; and &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt;:&lt;/p&gt;
&lt;p&gt;With the aforementioned conversion formulas for &lt;/p&gt;
&lt;hr&gt;
&lt;/div&gt;
&lt;/div&gt;
</ns0:encoded><pubDate>Wed, 27 May 2026 19:30:00 </pubDate></item><item><title>Scaling, stretching and shifting sinusoids</title><link>https://eli.thegreenplace.net/2026/scaling-stretching-and-shifting-sinusoids/</link><description>&lt;p&gt;This is a brief and simple &lt;a class="footnote-reference" href="#footnote-1" id="footnote-reference-1"&gt;[1]&lt;/a&gt; explanation of how to adjust the
standard sinusoid &lt;object class="valign-m5" data="https://eli.thegreenplace.net/images/math/1f2ea8ffab8deb0e5b854a260a68b42b7eb7b048.svg" style="height: 19px;" type="image/svg+xml"&gt;sin(x)&lt;/object&gt; to change its amplitude, frequency and
phase shift. More precisely, given the general function:&lt;/p&gt;
&lt;object class="align-center" data="https://eli.thegreenplace.net/images/math/85a45cfe4c1671765c3ab7e3949d8f74f780a0f3.svg" style="height: 19px;" type="image/svg+xml"&gt;\[s(x)=A\cdot sin(w\cdot x+\theta)\]&lt;/object&gt;
&lt;p&gt;We’ll see how adjusting the parameters &lt;img alt="A" class="valign-0" src="https://eli.thegreenplace.net/images/math/6dcd4ce23d88e2ee9568ba546c007c63d9131c1b.png" style="height: 12px;" /&gt;, &lt;img alt="w" class="valign-0" src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png" style="height: 8px;" /&gt; and
&lt;img alt="\theta" class="valign-0" src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png" style="height: 12px;" /&gt; affect the …&lt;/p&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;div class="entry-content" morss_own_score="5.914893617021276" morss_score="18.62173681196516"&gt;




&lt;p&gt;This is a brief and simple &lt;/p&gt;
&lt;p&gt;We’ll see how adjusting the parameters &lt;img src="https://eli.thegreenplace.net/images/math/6dcd4ce23d88e2ee9568ba546c007c63d9131c1b.png"&gt;, &lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt; and
&lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt; affect the shape of &lt;/p&gt;

&lt;h2&gt;Scaling&lt;/h2&gt;
&lt;p&gt;Scaling is conceptually the simplest change; we adjust &lt;img src="https://eli.thegreenplace.net/images/math/6dcd4ce23d88e2ee9568ba546c007c63d9131c1b.png"&gt; to
increase or decrease the amplitude (maximal height) of &lt;img src="https://eli.thegreenplace.net/images/math/95cb0bfd2977c761298d9624e4b4d4c72a39974a.png"&gt; value twice as large (in both the positive
and negative direction) as the original function.&lt;/p&gt;

&lt;div class="section" id="stretching" morss_own_score="2.954887218045113" morss_score="17.31203007518797"&gt;
&lt;h2&gt;Stretching&lt;/h2&gt;
&lt;p&gt;Stretching changes the frequency of &lt;img src="https://eli.thegreenplace.net/images/math/11f6ad8ec52a2984abaafd7c3b516503785c2072.png"&gt;.&lt;/p&gt;
&lt;p&gt;If we set &lt;img src="https://eli.thegreenplace.net/images/math/11f6ad8ec52a2984abaafd7c3b516503785c2072.png"&gt; is multiplied
by 2 before being fed into the sinusoid. If &lt;img src="https://eli.thegreenplace.net/images/math/11f6ad8ec52a2984abaafd7c3b516503785c2072.png"&gt; changes by
&lt;/p&gt;
&lt;p&gt;More generally, the period of &lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt;
and observing how the waveform changes.&lt;/p&gt;
&lt;p&gt;If we know the period &lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt; that gives us this period:&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Shifting&lt;/h2&gt;
&lt;p&gt;The final parameter we discuss is &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt;; it’s called the
&lt;em&gt;phase&lt;/em&gt; of the sinusoid. In the baseline &lt;img src="https://eli.thegreenplace.net/images/math/a1dffbe89f1ec5a919198de979fca459eb7fdf84.png"&gt;. The sinusoid is 0 at &lt;/p&gt;
&lt;p&gt;By adding a non-zero &lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt;, we don’t affect the sinusoid’s
amplitude or frequency, but we do shift it right or left along the
&lt;img src="https://eli.thegreenplace.net/images/math/11f6ad8ec52a2984abaafd7c3b516503785c2072.png"&gt; axis. For example, suppose we use the function
&lt;em&gt;left&lt;/em&gt; by
&lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt; is negative,
everything happens later, and the function is shifted &lt;em&gt;right&lt;/em&gt;.&lt;/p&gt;


&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;We’ve now gone over all the parameters for the function:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;img src="https://eli.thegreenplace.net/images/math/6dcd4ce23d88e2ee9568ba546c007c63d9131c1b.png"&gt; controls the scaling factor (amplitude).&lt;/li&gt;
&lt;li&gt;&lt;img src="https://eli.thegreenplace.net/images/math/aff024fe4ab0fece4091de044c58c9ae4233383a.png"&gt; is the frequency and controls the repetition period&lt;/li&gt;
&lt;li&gt;&lt;img src="https://eli.thegreenplace.net/images/math/cb005d76f9f2e394a770c2562c2e150a413b3216.png"&gt; controls the phase - how much the sinusoid is shifted
left or right&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use the demo below to adjust these parameters and observe their effect on
the sinusoid:&lt;/p&gt;

&lt;canvas&gt;
  Your browser does not support the HTML5 canvas tag.
  &lt;/canvas&gt;


&lt;label&gt;
        A
        &lt;/label&gt;
&lt;label&gt;
        ω
        &lt;/label&gt;
&lt;label&gt;
        θ
        &lt;/label&gt;




&lt;hr&gt;

&lt;/div&gt;
</ns0:encoded><pubDate>Sat, 02 May 2026 07:17:00 </pubDate></item><item><title>Thoughts on WebAssembly as a stack machine</title><link>https://eli.thegreenplace.net/2026/thoughts-on-webassembly-as-a-stack-machine/</link><description>&lt;p&gt;This week the article &lt;a class="reference external" href="https://purplesyringa.moe/blog/wasm-is-not-quite-a-stack-machine/"&gt;Wasm is not quite a stack machine&lt;/a&gt; has been
making the rounds and has caught my eye. The post claims that WASM is not a pure
stack machine because it has locals and is missing some stack manipulation
operations like &lt;tt class="docutils literal"&gt;dup&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;swap&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;While I don't …&lt;/p&gt;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;section id="content" morss_own_score="9.310344827586206" morss_score="12.568965517241379"&gt;
&lt;article morss_own_score="6.517241379310344" morss_score="11.38527656992618"&gt;

&lt;h1&gt;
&lt;a href="https://eli.thegreenplace.net/2026/thoughts-on-webassembly-as-a-stack-machine/" title="Permalink to Thoughts on WebAssembly as a stack machine"&gt;
                        Thoughts on WebAssembly as a stack machine
                    &lt;/a&gt;
&lt;/h1&gt;

&lt;div class="entry-content" morss_own_score="5.736070381231672" morss_score="43.966999286265406"&gt;




&lt;p&gt;This week the article &lt;a href="https://purplesyringa.moe/blog/wasm-is-not-quite-a-stack-machine/"&gt;Wasm is not quite a stack machine&lt;/a&gt; has been
making the rounds and has caught my eye. The post claims that WASM is not a pure
stack machine because it has locals and is missing some stack manipulation
operations like &lt;tt&gt;dup&lt;/tt&gt; and &lt;tt&gt;swap&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;While I don't necessarily disagree, IMHO it's a bit of a semantic
discussion because - to the best of my knowledge - there is no &lt;em&gt;formal&lt;/em&gt;
definition of what is a stack machine. Wikipedia, for example,
says:&lt;/p&gt;
&lt;blockquote&gt;
[...], a stack machine is a computer processor or a process virtual machine in
which the primary interaction is moving short-lived temporary values to and
from a push-down stack.&lt;/blockquote&gt;
&lt;p&gt;WASM certainly fits this definition; the &lt;em&gt;primary&lt;/em&gt; interaction is through the
stack, though WASM is augmented with an infinite register file (locals).
The more purist stack machines like Forth are only limited to the stack and a
memory (pointers into which are managed on the stack); WASM has these too, plus
the registers.&lt;/p&gt;
&lt;p&gt;Speaking of Forth, the mention of &lt;tt&gt;dup&lt;/tt&gt; reminded me of my own impressions
of programming in that language, documented in my post about
&lt;a href="https://eli.thegreenplace.net/2025/implementing-forth-in-go-and-c/"&gt;implementing Forth in Go and C&lt;/a&gt;. There,
I highlighted the following essential library function for Forth; it adds an
addend to a value stored in memory.&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;+!&lt;/span&gt;        &lt;span&gt;( addend addr -- )&lt;/span&gt;
  &lt;span&gt;tuck&lt;/span&gt;      &lt;span&gt;( addr addend addr )&lt;/span&gt;
  &lt;span&gt;@&lt;/span&gt;         &lt;span&gt;( addr addend value-at-addr )&lt;/span&gt;
  &lt;span&gt;+&lt;/span&gt;         &lt;span&gt;( addr updated-value )&lt;/span&gt;
  &lt;span&gt;swap&lt;/span&gt;      &lt;span&gt;( updated-value addr )&lt;/span&gt;
  &lt;span&gt;!&lt;/span&gt; &lt;span&gt;;&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;And lamented how difficult it is to understand such code without the
detailed stack view in comments alongside it.&lt;/p&gt;
&lt;p&gt;I find it much simpler to reason about this WASM code:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;func&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;export&lt;/span&gt; &lt;span&gt;"add_to_byte"&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;param&lt;/span&gt; &lt;span&gt;$addr&lt;/span&gt; &lt;span&gt;i32&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;param&lt;/span&gt; &lt;span&gt;$delta&lt;/span&gt; &lt;span&gt;i32&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
    &lt;span&gt;(&lt;/span&gt;&lt;span&gt;i32.store8&lt;/span&gt;
        &lt;span&gt;(&lt;/span&gt;&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$addr&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;
        &lt;span&gt;(&lt;/span&gt;&lt;span&gt;i32.add&lt;/span&gt;
            &lt;span&gt;(&lt;/span&gt;&lt;span&gt;i32.load8_u&lt;/span&gt; &lt;span&gt;(&lt;/span&gt;&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$addr&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;
            &lt;span&gt;(&lt;/span&gt;&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$delta&lt;/span&gt;&lt;span&gt;)))&lt;/span&gt;
&lt;span&gt;)&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;You may say this is cheating because folded WASM instructions help readability
and they're just syntactic sugar; OK, here's the linear code:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$addr&lt;/span&gt;
&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$addr&lt;/span&gt;
&lt;span&gt;i32.load8_u&lt;/span&gt;
&lt;span&gt;local.get&lt;/span&gt; &lt;span&gt;$delta&lt;/span&gt;
&lt;span&gt;i32.add&lt;/span&gt;
&lt;span&gt;i32.store8&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;It's still very readable, because - while the stack is used for all the
calculations and actual commands - some of the data lives in named "registers"
instead of on the stack. So we don't need all those tuck-swap contortions to get
things into the right order.&lt;/p&gt;
&lt;p&gt;One might worry about the duplicated &lt;tt&gt;local.get $addr&lt;/tt&gt;; wouldn't a real &lt;tt&gt;dup&lt;/tt&gt;
be better? Well, not in terms of readability, as we've already discussed. How
about performance? Since the stack VM is just an abstraction and the underlying
CPUs executing this code are register machines anyway, the answer is no - it
doesn't matter at all.&lt;/p&gt;
&lt;p&gt;Modern compiler engineers were forged in the fires of C and its descendants;
arbitrary control flow, arbitrary register and memory access, anything goes.
Compilers are quite sophisticated. Let's see how &lt;tt&gt;wasmtime&lt;/tt&gt; compiles our
&lt;tt&gt;add_to_byte&lt;/tt&gt; to native code (using &lt;tt&gt;wasmtime explore&lt;/tt&gt; with its
default &lt;tt&gt;&lt;span&gt;opt-level=2&lt;/span&gt;&lt;/tt&gt;); comments are added by me:&lt;/p&gt;
&lt;pre&gt;&lt;span&gt;// Prologue&lt;/span&gt;
&lt;span&gt;push&lt;/span&gt; &lt;span&gt;rbp&lt;/span&gt;
&lt;span&gt;mov&lt;/span&gt; &lt;span&gt;rbp&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;rsp&lt;/span&gt;

&lt;span&gt;// wasmtime's VM context pointer lives in rdi; 0x38 is likely its offset&lt;/span&gt;
&lt;span&gt;// to the default linear memory. Therefore, r10 will hold the base address&lt;/span&gt;
&lt;span&gt;// of the linear memory buffer&lt;/span&gt;
&lt;span&gt;mov&lt;/span&gt; &lt;span&gt;r10&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;qword&lt;/span&gt; &lt;span&gt;ptr&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;rdi&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; &lt;span&gt;0x38&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

&lt;span&gt;// The first parameter ($addr) is in edx; since WASM values are i32, it's&lt;/span&gt;
&lt;span&gt;// zero-extended into the 64-bit r11 by copying into r11d&lt;/span&gt;
&lt;span&gt;mov&lt;/span&gt; &lt;span&gt;r11d&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;edx&lt;/span&gt;

&lt;span&gt;// r10+r11 is memory[$addr]; this loads the current value into rsi&lt;/span&gt;
&lt;span&gt;// (zero-extending from 8 bits)&lt;/span&gt;
&lt;span&gt;movzx&lt;/span&gt; &lt;span&gt;rsi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;byte&lt;/span&gt; &lt;span&gt;ptr&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;r10&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; &lt;span&gt;r11&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;

&lt;span&gt;// ecx is the first parameter ($delta); this adds the addend to the&lt;/span&gt;
&lt;span&gt;// current value&lt;/span&gt;
&lt;span&gt;add&lt;/span&gt; &lt;span&gt;esi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;ecx&lt;/span&gt;

&lt;span&gt;// Store cur_value+addend back into memory[$addr]&lt;/span&gt;
&lt;span&gt;mov&lt;/span&gt; &lt;span&gt;byte&lt;/span&gt; &lt;span&gt;ptr&lt;/span&gt; &lt;span&gt;[&lt;/span&gt;&lt;span&gt;r10&lt;/span&gt; &lt;span&gt;+&lt;/span&gt; &lt;span&gt;r11&lt;/span&gt;&lt;span&gt;],&lt;/span&gt; &lt;span&gt;sil&lt;/span&gt;

&lt;span&gt;// Epilogue&lt;/span&gt;
&lt;span&gt;mov&lt;/span&gt; &lt;span&gt;rsp&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;rbp&lt;/span&gt;
&lt;span&gt;pop&lt;/span&gt; &lt;span&gt;rbp&lt;/span&gt;
&lt;span&gt;ret&lt;/span&gt;
&lt;/pre&gt;
&lt;p&gt;This is pretty much the code we'd expect to be emitted for the C statement
&lt;tt&gt;mem[addr] += addend&lt;/tt&gt;, or if we were writing x86-64 assembly by hand. The
compiler had no difficulty figuring out that two consecutive loads from
the same WASM local produce the same value and do not - in fact - have to be
duplicated. The WASM model makes it rather easy, because you can't alias locals;
as long as there are no intervening writes into the same local, multiple reads
are known to produce the same value (redundant load elimination).&lt;/p&gt;
&lt;/div&gt;
&lt;hr&gt;

&lt;p&gt;
For comments, please send me
&lt;/p&gt;
 &lt;/article&gt;
&lt;/section&gt;
</ns0:encoded><pubDate>Wed, 29 Apr 2026 19:28:00 </pubDate></item></channel></rss>