I've dipped into agentic work now and again, but never been very impressed with the output (well, that there is any functioning output is insanely impressive, but it isn't code I want to be on the hook for complaining).
I hear a lot of people saying the same, but similarly a bunch of people I respect saying they barely write code anymore. It feels a little tricky to square these up sometimes.
Anyway, really looking forward to trying some if these patterns as the book develops to see if that makes a difference. Understanding how other peopke really use these tools is a big gap for me.
In my experience, this heavily depends on the task, and there's a massive chasm between tasks where it's a good and bad fit. I can definitely imagine people working only on one side of this chasm and being perplexed by the other side.
So, even though they want the code base to be in Rust, they will continue writing new code in C++ and then translating it to Rust later to remove the C++ they just added.
There must be a really good reason for this, such as Rust doesn’t interop well with C++, otherwise this seems like a poor decision.
Yea, I'd bet it's that. Ideally, you'd want to stop writing C++ and continue with Rust on all new code, but Rust has stricter semantics, so the interop is somewhat "easy" in one direction, but very hairy in the other direction.
This means that in practice, you want to start porting from leaf components and slowly grow closer to the root, which stays in C++ for quite some time and just calls into Rust through C API (or something close to it).
If you're curious about the topic, there's a interop library called Zngur (https://hkalbasi.github.io/zngur/), which is built on this assumption. They have a pretty good explanation of the concrete problems on the homepage.
I'm confused about this part. What part of the browser did they want GC and inheritance for? I'd get it if they were writing the UI in this, but the rest of this post is about the JS engine. They weren't going to 1:1 map JS objects to Swift objects and rely on ARC to manage memory, were they?
You can model these with reference counting, but this turned out unfeasible in browsers. There's a great talk from when Blink (Chrome) transitioned from reference counting to GC, which provides a lot more details about these problems in practice: https://www.youtube.com/watch?v=_uxmEyd6uxo
> I'd get it if they were writing the UI in this, but the rest of this post is about the JS engine.
I think this might be the reason they started with the JS engine and not with some more fundamental browser structures. JS object model has these problems, too, but the engine has to solve them in more generic way. All JS objects can just be modeled as some JSObject class/struct where this is handled on the engine level.
DOM and other browser structures are different because the engine has to understand them, so the browser developers have to interact with the GC manually and if you watch the talk above, you'll see that it's quite involved to do even in C++, let alone in Rust, which puts a bunch of restrictions on top of that.
If you ask me, Go is a better Rust. Rust is an ugly version of C++ with longer compile times and a band of zealous missionaries.
I mean the keywords mut and fn very annoying to read just get rid of them or spell the f*n thing function.
I always wonder how can one come to such a conclusion. Modern C++ has no way to enforce relationship between two objects in memory and the shared xor mutable rule, which means it can't even do the basic checks that are the foundation of Rust's safety features.
Of course, this statement is also trivially debunked by the reality of any major C++ program with complexity and attack surface of something like a browser. Modern C++ certainly didn't save Chrome from CVEs. They ban a bunch of C++ features, enforce the rule of two, and do a bunch of hardening and fuzzing on top of it and they still don't get spared from safety issues.
The article fails to explain why. What problems (besides the obvious) have been found in which "memory-safe languages" can help. Do these problems actually explain the need of adding complexity to a project like this by adding another language?
I guess AI will be involved which, at this early point in the project would make ladybird a lot less interested (at least to me).
Well, what else is there besides the obvious? It's a browser.
The paradox gains another layer when you consider that their whole mission is to build tools for the JavaScript ecosystem, yet by moving to Rust they are betting that JS-the-language is so broken that it cannot even host its own tools. And because JS is still a stronger language for building UIs in than Rust, their business strategy now makes them hard-committed to their bet that JS tools in JS are a dead end.
Also, the paradox is not really even there. JS ecosystem largely gave up on JS tools long time ago already. Pretty much all major build tools are migrating to native or already migrated, at least partially. This has been going on for last 4 years or something.
But the key to all of this is that most of these tools are still supporting JS plugins. Rolldown/Vite is compatible with Rollup JS plugins and OXLint has ESLint compatible API (it's in preview atm). So it's not really even a bet at all.
Of course text is so universal and allows for so many ways of editing that it's hard to give up. On the other hand, while text is great for input, it comes with overhead and core issues for (most are already in the article, but I'm writing them down anyway):
1. Substitutions such as renaming a symbol where ensuring the correctness of the operation pretty much requires having parsed the text to a graph representation first, or letting go of the guarantee of correctness in the first place and performing plain text search/replace.
2. Alternative representations requiring full and correct re-parsing such as:
- overview of flow across functions
- viewing graph based data structures, of which there tend to be many in a larger application
- imports graph and so on...
3. Querying structurally equivalent patterns when they have multiple equivalent textual representations and search in general being somewhat limited.
4. Merging changes and diffs have fewer guarantees than compared to when merging graphs or trees.
5. Correctness checks, such as cyclic imports, ensuring the validity of the program itself are all build-time unless the IDE has effectively a duplicate program graph being continuously parsed from the changes that is not equivalent to the eventual execution model.
6. Execution and build speed is also a permanent overhead as applications grow when using text as the source. Yes, parsing methods are quite fast these days and the hardware is far better, but having a correct program graph is always faster than parsing, creating & verifying a new one.
I think input as text is a must-have to start with no matter what, but what if the parsing step was performed immediately on stop symbols rather than later and merged with the program graph immediately rather than during a separate build step?Or what if it was like "staging" step? Eg, write a separate function that gets parsed into program model immediately, then try executing it and then merge to main program graph later that can perform all necessary checks to ensure the main program graph remains valid? I think it'd be more difficult to learn, but I think having these operations and a program graph as a database, would give so much when it comes to editing, verifying and maintaining more complex programs.
I think this is the way to go, kinda like on Github, where you write markdown in the comments, but that is only used for input, after that it's merged into the system, all code-like constructs (links, references, images) are resolveed and from then you interact with the higher level concept (rendered comment with links and images).
For programinng langauge, Unison does this - you write one function at a time in something like a REPL and functions are saved in content addressed database.
> Or what if it was like "staging" step?
Yes, and I guess it'd have to go even deeper. The system should be able to represent broken program (in edited state), so conceptually it has to be something like a structured database for code which separates the user input from stored semantic representation and the final program.
IDE's like IntelliJ already build a program model like this and incrementally update it as you edit, they just have to work very hard to do it and that model is imperfect.
There's million issues to solve with this, though. It's a hard problem.
> Dion language demo (experimental project which stores program as AST).
Michael Franz [1] invented slim binaries [2] for the Oberon System. Slim binaries were program (or module) ASTs compressed with the some kind of LZ-family algorithm. At the time they were much more smaller than Java's JAR files, despite JAR being a ZIP archive.[1] https://en.wikipedia.org/wiki/Michael_Franz#Research
[2] https://en.wikipedia.org/wiki/Oberon_(operating_system)#Plug...
I believe that this storage format is still in use in Oberon circles.
Yes, I am that old, I even correctly remembered Franz's last name. I thought then he was and still think he is a genius. ;)
Dion project was more about user interface to the programming language and unifying tools to use AST (or Typed AST?) as a source of truth instead of text and what that unlocks.
Dion demo is here: https://vimeo.com/485177664
With Kotlin/Spring Boot, compilation is annoyingly slow. That's what you get with modern languages and rich syntax. Apparently the Rust compiler isn't a speed daemon either. But tests are something that's under your control. Unit tests should be done in seconds/milliseconds. Integration tests are where you can make huge gains if you are a bit smart.
Most integration tests are not thread safe and make assumptions about running against an empty database. Which if you think about it, is exactly how no user except your first user will ever use your system.
The fix for this is 1) allow no cleanup between tests 2) randomize data so there are no test collisions between tests and 3) use multiple threads/processes to run your tests to 1 database that is provisioned before the tests and deleted after all tests.
I have a fast mac book pro that runs our hundreds of spring integration tests (proper end to end API tests with redis, db, elasticsearch and no fakes/stubs) in under 40 seconds. It kind of doubles as a robustness and performance test. It's fast enough that I have codex just trigger that on principle after every change it makes.
There's a bit more to it of course (e.g. polling rather than sleeping for assertions, using timeouts on things that are eventually happening, etc.). But once you have set this up once, you'll never want to deal with sequentially running integration tests again. Having to run those over and over again just sucks the joy out of life.
And with agentic coding tools having fast feedback loops is more critical than ever.
Yea, cypress has this in their anti-patterns:
https://docs.cypress.io/app/core-concepts/best-practices#Usi...
Dangling state is useful for debugging when the test fails, you don't want to clean that up.
This has been super useful practice in my experience. I really like to be able to run tests regardless of my application state. It's faster and over time it helps you hit and fixup various issues that you only encounter after you fill the database with enough data.