Ranjan Roy from Margins is back for our weekly discussion of the latest tech news. We cover: 1) The WSJ says the Hugging Face might not be all that it was cracked up to be 2) The bots were told to hack? 3) Was this all a false flag for an effective altrust takeover? 4) Was this a smoke screen to conceal bad financial numbers from the AI labs? 5) Alex: Actually, maybe we should take these issues seriously? 6) Ranjan: They why aren't the labs being more transparent 7) Astra-6's crazy instructions to alter its personality in training 8) Anthropic hits $100 million in ARR 9) The IPOs will go on 10) The frontier AI business shows signs of slowing
---
Enjoying Big Technology Podcast? Please rate us five stars âââââ in your podcast app of choice.
Want a discount for Big Technology on Substack + Discord? Hereâs 25% off for the first year: https://www.bigtechnology.com/subscribe?coupon=0843016b
Learn more about your ad choices. Visit megaphone.fm/adchoices
Most AI tooling I've used in the last couple of years follows the same pattern. You send a prompt, get back text, and then write brittle code to turn that text into something your app can actually use. JSON mode helps. Function calling helps more. Even then, you are still asking a model trained to write for people to produce machine-readable output, and hoping it doesn't wrap the JSON in a markdown fence, drop a field, or invent a category that is not in your enum.
Two days ago I found TypeSafe and its model, Jev. It takes a different approach. Instead of generating text, it evaluates typed questions against a piece of input and returns structured, typed decisions. No parsing. No "please respond only with valid JSON." I liked the idea enough that I had GitHub Copilot with HydraFusion port TypeSafe's official TypeScript SDK to .NET 11 and C# 15. This post is part introduction to the model and part walkthrough of the ported TypeSafe .NET SDK, so you can try it in C# yourself.
Are generative (randomized) tests significantly more effective than example-based unit-tests at discovering bugs? Thereâs an interesting discussion about this on lobste.rs. One argument in favor of unit tests is, paraphrasing
My generic fuzzer wasnât able to find this tricky bug in Rust
regexcrate.
To me, it seems that generative testing should shake out that particular
creature, so I wrote a lil fuzzer
of my own, and it indeed discovered another bug in that version of regex,
and then the one I was after. I didnât find anything in the latest version. I
like to do a write up about the process, as it is a good case study for how one
approaches a problem like this.
I want to be extra clear that my argument is very weak here, as I know exactly the bug I am after, and I even know that fuzzers can find it. My primary goal is to teach you the techniques, leaving it to your judgment just how effective they are. That being said, I think finding a second bug validates the approach somewhat.
I also want to emphasize that writing fuzzers to find known bugs is far from an idle amusement. While I believe that generative testing is very powerful, relative to its cost, itâs always a question whether a particular test is throughout enough. And it never is, you will find more bugs elsewhere (thatâs why defense in depth and runtime mitigations are critical). And, whenever you have a pest that dodged your fuzzers, your first order of business is to treat this event as a bug in the fuzzer, and change it so that it can find this and related bugs. Only then you are allowed to add a fix and a unit test!
For ".abb|b" regex and "zabb" input, an older version of regex crate
returned b as the first match, which is incorrect, because the entire zabb
matches:
use regex;
fn main() {
let r = regex::Regex::new(".abb|b").unwrap();
let m = r.find("zabb").unwrap();
// Fails with regex-automata=0.4.15:
assert_eq!(m.as_str(), "zabb")
}
How do we find this, or something like this?
Regular expression engines are one of the easiest things to apply generative testing to, they are pure algorithms. While few large systems are just an algorithm, algorithms are everywhere inside components of interesting systems, so this is a hands-on knowledge.
And by far the most important technique for testing algorithms is to compare
with the known right answer, with an oracle. Implement both O(N log N) and
O(N^2) versions of the algorithm, and match the answers.
To be fair, the original comment mentioned that the their fuzzer didnât find the issue because they didnât have access to an oracle. However, if you are designing a reliable system, itâs part of your job to ensure it has an oracle! One of the first things we did for our Jepsen test at TigerBeetle was to expose internal timestamps via API, to make it easier for Jepsen to find bugs (TigerBeetle is co-designed with its internal simulator VOPR which naturally has access to timestamps and anything else). And for, a regex engine, coming up with an oracle shouldnât be hard, as they typically already come with multiple specialized implementations under a single facade, and the implementations can be cross-checked against each other.
But the regex case is even simpler (which makes it an excellent case study).
Thereâs regex_lite crate that provides the same API.
So hereâs a plan: generate a regular expression, an input text, and check that
regex and regex_lite give identical answers.
Iâll start with code that generates a random string, as it is simpler, but still shows some non-trivial ideas. First, weâll need a random number generator:
use fastrand::Rng;
There are fancier techniques, which can give you test-case minimization, exhaustive search, or coverage guided exploration, but the insight is that even a humble PRNG is brutally effective, if you put it to good use.
When you start with randomized testing, the instinct is to generate something big, no, HUGE! Surely regex will choke on 5 GiBs of input? This is usually a wrong call. Bugs usually involve small, but tricky examples, weaponizing interactions between a few features. A string where all characters are the same is more likely to trigger a bug than a purely random string where every character is unique.
So my default approach to generating strings is this. First, I fix the
alphabet of possible characters. A nice way to get one is to sort | unique all
the unit tests. Then, for each particular string, I pick a subset of that
alphabet. I want strings that use all the characters, but I also want long
strings with only a and b! Then I generate a string using the given subset
of the alphabet, where the length of the string is also picked at random.
To make fuzzing efficient, I want to keep each iteration as fast as possible, so I make sure to re-use the memory across iterations, static allocation in the small:
use fastrand::Rng;
fn main() {
let mut rng = Rng::new();
// Re-use the same memory for all tests.
let mut text_alphabet: Vec<u8> = vec![];
let mut text: Vec<u8> = vec![];
for _ in 0..1_000_000 {
// It's unlikely that a counter example with
// 7 different letters exists, while there
// isn't one with just 6.
alphabet_swarm(&mut rng, b"abcdef", &mut text_alphabet);
let text =
gen_string(&mut rng, &text_alphabet, &mut text);
}
}
fn alphabet_swarm<'a>(
rng: &mut Rng,
all: &[u8],
pick: &'a mut Vec<u8>,
) {
pick.clear();
pick.extend(all);
rng.shuffle(pick);
let count = rng.usize(1..=pick.len());
pick.truncate(count);
}
fn gen_string<'a>(
rng: &mut Rng,
alphabet: &[u8],
result: &'a mut Vec<u8>,
) -> &'a str {
result.clear();
// Again, this is a short string.
// Longer failures are not likely.
let count = rng.usize(0..8);
for _ in 0..count {
result.push(alphabet[rng.usize(0..alphabet.len())]);
}
str::from_utf8(result).unwrap()
}
Thereâs a nice way to think about this two step process, generating alphabet first, and then generating a string. To generate a string, you need a distribution of characters. You can use the same distribution for each of the million iterations. But an easy way to spice things up is to make the distribution itself random. I file this ârandomize distributions themselvesâ idea under swarm testing.
Letâs apply the same tricks when generating a regex:
Letâs start with the first one:
struct ReOptions {
alt: u16, // |
rep: u16, // *
any: u16, // .
lit: u16, // 'a'
sum: u16,
alphabet: Vec<u8>,
}
Regexes have alternation r1|r2, repetition r*, wildcard ., and literals
a. Rather then binary enabling or disabling a particular feature, I assign
each feature a weight between 0 and 100, which is a bit more general. The sum
is the total of all weights. To select a feature at random, we need to generate
a number in 0..sum and find which segment it falls into.
In anything more serious, Iâd introduce explicit types for probabilities and distributions, but just a two-digit number is perfectly serviceable in the small.
This is how I generate ReOptions, making sure that literals always have
non-zero weight, and also selecting an alphabet for them:
impl ReOptions {
fn swarm(&mut self, rng: &mut Rng, alphabet_full: &[u8]) {
// We _still_ want to enable a few features at a time.
self.alt = if rng.bool() { 0 } else { rng.u16(0..100) };
self.rep = if rng.bool() { 0 } else { rng.u16(0..100) };
self.any = if rng.bool() { 0 } else { rng.u16(0..100) };
self.lit = rng.u16(1..100);
self.sum = self.alt + self.rep + self.any + self.lit;
assert!(self.sum > 0);
alphabet_swarm(rng, alphabet_full, &mut self.alphabet);
}
}
So now we can generate a regular expression. This is convenient to do
recursively. To avoid allocations, an output buffer is passed through. To
control regex length, a size parameter is also threaded, and âbranchingâ
recursive invocations divide the size between the children:
fn gen_re(
rng: &mut Rng,
options: &ReOptions,
result: &mut Vec<u8>,
) {
result.clear();
let size = rng.u8(0..8);
gen_re_rec(rng, options, result, size);
}
fn gen_re_rec(
rng: &mut Rng,
options: &ReOptions,
result: &mut Vec<u8>,
size: u8,
) {
if size == 0 {
return; // Base case, empty regex.
}
// Pick one of the features, according to weights.
let mut p = rng.u16(0..options.sum);
if p < options.alt {
// Alternation distributes the size
// among the two children.
let size_left = rng.u8(0..=size - 1);
let size_right = size - size_left - 1;
assert!(size == size_left + 1 + size_right);
result.push(b'(');
gen_re_rec(rng, options, result, size_left);
result.extend(b")|(");
gen_re_rec(rng, options, result, size_right);
result.push(b')');
return;
}
p -= options.alt;
if p < options.rep {
result.push(b'(');
gen_re_rec(rng, options, result, size - 1);
result.extend(b")*");
return;
}
p -= options.rep;
if p < options.any {
gen_re_rec(rng, options, result, size - 1);
result.push(b'.');
return;
}
p -= options.any;
if p < options.lit {
gen_re_rec(rng, options, result, size - 1);
let index = rng.usize(0..options.alphabet.len());
let lit = options.alphabet[index];
result.push(lit);
return;
}
unreachable!();
}
Given that compiling regular expressions is somewhat slow, it seems like a good idea to try multiple strings for the same pair of regular expressions, which gives the following code:
fn main() {
let mut rng = Rng::new();
let mut options = ReOptions::default();
let mut text_alphabet: Vec<u8> = vec![];
let mut text: Vec<u8> = vec![];
let mut re: Vec<u8> = vec![];
let mut test_count: u32 = 0;
for _ in 0..1_000_000 {
options.swarm(&mut rng, b"abcdef");
alphabet_swarm(&mut rng, b"abcdefx", &mut text_alphabet);
gen_re(&mut rng, &options, &mut re);
let re = str::from_utf8(&re).unwrap();
let r1 = regex::Regex::new(re).unwrap();
let r2 = regex_lite::Regex::new(re).unwrap();
for _ in 0..1000 {
test_count += 1;
let text =
gen_string(&mut rng, &text_alphabet, &mut text);
let m1 = r1.find(text)
.map_or("not found", |it| it.as_str());
let m2 = r2.find(text)
.map_or("not found", |it| it.as_str());
if m1 != m2 {
eprintln!("err re={re} text={text} m1={m1} m2={m2}");
return;
}
if test_count % 500_000 == 0 {
eprintln!("ok re={re} text={text}");
}
}
}
}
It produces examples similar to those in the issue, with a common suffix:
err re=(e)|(fee) text=xxfee
but also examples which somewhat different, without the shared suffix:
err re=(f..)*.d text=xfcbdd
All together:
use fastrand::Rng;
fn main() {
let mut rng = Rng::new();
let mut options = ReOptions::default();
let mut text_alphabet: Vec<u8> = vec![];
let mut text: Vec<u8> = vec![];
let mut re: Vec<u8> = vec![];
let mut test_count: u32 = 0;
for _ in 0..1_000_000 {
options.swarm(&mut rng, b"abcdef");
alphabet_swarm(&mut rng, b"abcdefx", &mut text_alphabet);
gen_re(&mut rng, &options, &mut re);
let re = str::from_utf8(&re).unwrap();
let r1 = regex::Regex::new(re).unwrap();
let r2 = regex_lite::Regex::new(re).unwrap();
for _ in 0..1000 {
test_count += 1;
let text =
gen_string(&mut rng, &text_alphabet, &mut text);
let m1 = r1.find(text)
.map_or("not found", |it| it.as_str());
let m2 = r2.find(text)
.map_or("not found", |it| it.as_str());
if m1 != m2 {
eprintln!("err re={re} text={text} m1={m1} m2={m2}");
return;
}
if test_count % 500_000 == 0 {
eprintln!("ok re={re} text={text}");
}
}
}
}
fn alphabet_swarm<'a>(
rng: &mut Rng,
all: &[u8],
pick: &'a mut Vec<u8>,
) {
pick.clear();
pick.extend(all);
rng.shuffle(pick);
let count = rng.usize(1..=pick.len());
pick.truncate(count);
}
fn gen_string<'a>(
rng: &mut Rng,
alphabet: &[u8],
result: &'a mut Vec<u8>,
) -> &'a str {
result.clear();
let count = rng.usize(0..8);
for _ in 0..count {
result.push(alphabet[rng.usize(0..alphabet.len())]);
}
str::from_utf8(result).unwrap()
}
struct ReOptions {
alt: u16, // |
rep: u16, // *
any: u16, // .
lit: u16, // 'a'
sum: u16,
alphabet: Vec<u8>,
}
impl ReOptions {
fn swarm(&mut self, rng: &mut Rng, alphabet_full: &[u8]) {
self.alt = if rng.bool() { 0 } else { rng.u16(0..100) };
self.rep = if rng.bool() { 0 } else { rng.u16(0..100) };
self.any = if rng.bool() { 0 } else { rng.u16(0..100) };
self.lit = rng.u16(1..100);
self.sum = self.alt + self.rep + self.any + self.lit;
assert!(self.sum > 0);
alphabet_swarm(rng, alphabet_full, &mut self.alphabet);
}
}
fn gen_re(
rng: &mut Rng,
options: &ReOptions,
result: &mut Vec<u8>,
) {
result.clear();
let size = rng.u8(0..8);
gen_re_rec(rng, options, result, size);
}
fn gen_re_rec(
rng: &mut Rng,
options: &ReOptions,
result: &mut Vec<u8>,
size: u8,
) {
if size == 0 {
return; // Base case, empty regex.
}
// Pick one of the features, according to weights.
let mut p = rng.u16(0..options.sum);
if p < options.alt {
// Alternation distributes the size
// among the two children.
let size_left = rng.u8(0..=size - 1);
let size_right = size - size_left - 1;
assert!(size == size_left + 1 + size_right);
result.push(b'(');
gen_re_rec(rng, options, result, size_left);
result.extend(b")|(");
gen_re_rec(rng, options, result, size_right);
result.push(b')');
return;
}
p -= options.alt;
if p < options.rep {
result.push(b'(');
gen_re_rec(rng, options, result, size - 1);
result.extend(b")*");
return;
}
p -= options.rep;
if p < options.any {
gen_re_rec(rng, options, result, size - 1);
result.push(b'.');
return;
}
p -= options.any;
if p < options.lit {
gen_re_rec(rng, options, result, size - 1);
let index = rng.usize(0..options.alphabet.len());
let lit = options.alphabet[index];
result.push(lit);
return;
}
unreachable!();
}
https://github.com/matklad/regex-fuzz
Takeaways:
Being good at building and being good at shipping are two separate skills. In the short term, theyâre actually countervailing: if you have a gift for building, youâre likely to be worse at shipping. Ira Glass has a classic quote about this.
All of us who do creative work, we get into it because we have good taste. But there is this gap. For the first couple years you make stuff, itâs just not that good. Itâs trying to be good, it has potential, but itâs not. But your taste, the thing that got you into the game, is still killer. And your taste is why your work disappoints you.
The only way around this is to grit your teeth and ship it. You have to force yourself to publish things youâve made even when you think theyâre crap.
Gifted programmers have a nearly pathological desire to build elegant, correct, neat systems. Thatâs what motivates them to learn the arcane details of their languages, or to spend time polishing and refactoring over and over again. But itâs also what makes them reluctant to ship. Any flaws in the software bother them on an emotional level. If they ship with those flaws, they feel like people will think they werenât paying enough attention to notice them, or that they werenât good enough to fix them.
This is annoying when youâre writing software on your own, but itâs completely fatal when youâre working in a tech company. Any large software system is covered in flaws, whether due to time pressure, relative inexperience, wicked features, or a hundred other reasons. Working with it is a process of compromise: of finding the best possible solution given the quirks and foibles of the codebase. In fact, since the most important thing in large codebases is consistency, the right thing to do is sometimes to duplicate flaws, assuming theyâre not catastrophic.
Gifted programmers often freeze up. Iâve often seen them retreat to smaller domains where they can safely make the code âcorrectâ: tweaking dev-environment setup, or refactoring tests. Sometimes they just do nothing, and spin in shame and guilt (plus the compounding shame of not achieving anything) until they implode and quit. If they had worse taste, they wouldnât be as good at programming, but theyâd be a lot more useful. You can typically improve a bad diff with time and effort. You canât improve no diff.
I have a sensitive eye for awkward sentences and uneven prose. That can make writing an unpleasant process: I know what Iâm trying to say, but I canât seem to say it in a way thatâs as clear and as elegant as I know is possible. More than half the time I finish drafting a blog post, I look at the post and donât think itâs very good. But I (mostly) grit my teeth and publish it anyway, because you have to bias towards shipping.
Like any skill, shipping gets easier the more you practice it. If I donât publish a blog post for a month, I always feel like the next draft is too poorly-written or uninteresting to put out there. But when Iâm publishing a post per day, I typically feel great about each draft. When I go back and read my old posts, I canât tell which ones I felt good about and which ones I felt bad about. Thereâs no correlation between that and the posts that become popular. Here are some posts I didnât like as I was writing them but that resonated with my audience:
Here are some posts I thought were pretty good but that didnât find popularity:
You just canât predict what people will find interesting or useful. Producing a high volume of work thus gives much better yield than a small amount of highly-polished work.
It can be disheartening to realize that some of your most casual, throwaway work will be more successful than the work you slaved over1. Specifically, itâs disheartening because it means realizing you donât have control over your own success. You canât produce something successful by focusing on a single piece until youâre satisfied itâs great. Instead, you just have to do a lot of things and see what sticks. You have to be momentum-based, not outcome-based. In other words, you have to grit your teeth and ship it.
One common reason to write less is getting overly precious about your ideas. If you think youâve got a really compelling concept, you donât want to âwaste itâ on a poorly-written story. But in fact you can just write about the same thing over and over until you get it right! I have written like thirty blog posts about shipping (this is one of them), or about how tech companies work, or about how internal emotional regulation is as important as technical ability. I expect to continue writing and thinking about these ideas for as long as I find them interesting.
Build a bounded TPL Dataflow pipeline in .NET 10 with explicit capacity, concurrency, ordering, cancellation, coordinated faults, and terminal completion.