From 0045769e6f8c72f514ac87bc02e722b3b114ad8a Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 17 Feb 2026 21:44:59 -0800 Subject: [PATCH 1/6] fix mobile benchmark overlap and add zero-cost c interop feature --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 501eb469..64d4fd79 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,10 +27,10 @@ features: - - + +
## Ready to try it? From e774d6cda9604794e480bff3e7a2d734332bd762 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Tue, 17 Feb 2026 22:42:53 -0800 Subject: [PATCH 2/6] interactive pipeline showcase with hexagon, ir highlighting, and typewriter animations --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 64d4fd79..501eb469 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,10 +27,10 @@ features: - - + +
## Ready to try it? From 4184fdebe533bb3ab6bd2dfa9de92973a34832e9 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 19 Feb 2026 11:01:27 -0800 Subject: [PATCH 3/6] fix set buffer overflow with dedup and dynamic reallocation --- src/codegen/types/collections/set.ts | 191 ++++++++++++++++++++++++--- 1 file changed, 173 insertions(+), 18 deletions(-) diff --git a/src/codegen/types/collections/set.ts b/src/codegen/types/collections/set.ts index aa40fcba..d894fa8a 100644 --- a/src/codegen/types/collections/set.ts +++ b/src/codegen/types/collections/set.ts @@ -92,19 +92,14 @@ export class SetGenerator { } generateSetAdd(expr: MethodCallNode, params: string[]): string { - // set.add(value) if (expr.args.length !== 1) { throw new Error('Set.add() requires exactly 1 argument'); } - // Get set pointer const setPtr = this.ctx.generateExpression(expr.object, params); - - // Generate value const valueToAdd = this.ctx.generateExpression(expr.args[0], params); + const dblValue = this.ctx.ensureDouble(valueToAdd); - // Check if value already exists (simple linear search) - // Load current array and size const valuesFieldPtr = this.nextTemp(); this.emit(`${valuesFieldPtr} = getelementptr inbounds %Set, %Set* ${setPtr}, i32 0, i32 0`); const valuesPtr = this.nextTemp(); @@ -115,20 +110,96 @@ export class SetGenerator { const currentSize = this.nextTemp(); this.emit(`${currentSize} = load i32, i32* ${sizeFieldPtr}`); - // For simplicity, assume value doesn't exist and just add it - // (In production, we'd check for duplicates) - - // Store value at index = currentSize - const valueElemPtr = this.nextTemp(); - this.emit(`${valueElemPtr} = getelementptr inbounds double, double* ${valuesPtr}, i32 ${currentSize}`); - this.emit(`store double ${this.ctx.ensureDouble(valueToAdd)}, double* ${valueElemPtr}`); + const dedupLoop = this.nextLabel('set_add_dedup'); + const dedupBody = this.nextLabel('set_add_dedup_body'); + const dedupNext = this.nextLabel('set_add_dedup_next'); + const alreadyExists = this.nextLabel('set_add_exists'); + const dedupDone = this.nextLabel('set_add_dedup_done'); + const resizeLabel = this.nextLabel('set_add_resize'); + const doInsert = this.nextLabel('set_add_insert'); + const endLabel = this.nextLabel('set_add_end'); + + const idxReg = this.nextTemp(); + this.emit(`${idxReg} = alloca i32`); + this.emit(`store i32 0, i32* ${idxReg}`); + this.emit(`br label %${dedupLoop}`); + + this.emit(`${dedupLoop}:`); + const curIdx = this.nextTemp(); + this.emit(`${curIdx} = load i32, i32* ${idxReg}`); + const idxCond = this.nextTemp(); + this.emit(`${idxCond} = icmp slt i32 ${curIdx}, ${currentSize}`); + this.emit(`br i1 ${idxCond}, label %${dedupBody}, label %${dedupDone}`); + + this.emit(`${dedupBody}:`); + const elemPtr = this.nextTemp(); + this.emit(`${elemPtr} = getelementptr inbounds double, double* ${valuesPtr}, i32 ${curIdx}`); + const elemVal = this.nextTemp(); + this.emit(`${elemVal} = load double, double* ${elemPtr}`); + const match = this.nextTemp(); + this.emit(`${match} = fcmp oeq double ${elemVal}, ${dblValue}`); + this.emit(`br i1 ${match}, label %${alreadyExists}, label %${dedupNext}`); + + this.emit(`${dedupNext}:`); + const nextIdx = this.nextTemp(); + this.emit(`${nextIdx} = add i32 ${curIdx}, 1`); + this.emit(`store i32 ${nextIdx}, i32* ${idxReg}`); + this.emit(`br label %${dedupLoop}`); + + this.emit(`${alreadyExists}:`); + this.emit(`br label %${endLabel}`); - // Increment size + this.emit(`${dedupDone}:`); + const capFieldPtr = this.nextTemp(); + this.emit(`${capFieldPtr} = getelementptr inbounds %Set, %Set* ${setPtr}, i32 0, i32 2`); + const currentCap = this.nextTemp(); + this.emit(`${currentCap} = load i32, i32* ${capFieldPtr}`); + const needResize = this.nextTemp(); + this.emit(`${needResize} = icmp eq i32 ${currentSize}, ${currentCap}`); + this.emit(`br i1 ${needResize}, label %${resizeLabel}, label %${doInsert}`); + + this.emit(`${resizeLabel}:`); + const isZero = this.nextTemp(); + this.emit(`${isZero} = icmp eq i32 ${currentCap}, 0`); + const doubled = this.nextTemp(); + this.emit(`${doubled} = mul i32 ${currentCap}, 2`); + const newCap = this.nextTemp(); + this.emit(`${newCap} = select i1 ${isZero}, i32 4, i32 ${doubled}`); + const newCapI64 = this.nextTemp(); + this.emit(`${newCapI64} = zext i32 ${newCap} to i64`); + const newMemSize = this.nextTemp(); + this.emit(`${newMemSize} = mul i64 ${newCapI64}, ${this.getDoubleSize()}`); + const newMem = this.nextTemp(); + this.emit(`${newMem} = call i8* @GC_malloc_atomic(i64 ${newMemSize})`); + const newDataPtr = this.nextTemp(); + this.emit(`${newDataPtr} = bitcast i8* ${newMem} to double*`); + const oldDataI8 = this.nextTemp(); + this.emit(`${oldDataI8} = bitcast double* ${valuesPtr} to i8*`); + const newDataI8 = this.nextTemp(); + this.emit(`${newDataI8} = bitcast double* ${newDataPtr} to i8*`); + const currentSizeI64 = this.nextTemp(); + this.emit(`${currentSizeI64} = zext i32 ${currentSize} to i64`); + const copySize = this.nextTemp(); + this.emit(`${copySize} = mul i64 ${currentSizeI64}, ${this.getDoubleSize()}`); + this.emit(`call void @llvm.memcpy.p0i8.p0i8.i64(i8* ${newDataI8}, i8* ${oldDataI8}, i64 ${copySize}, i1 false)`); + this.emit(`store double* ${newDataPtr}, double** ${valuesFieldPtr}`); + this.emit(`store i32 ${newCap}, i32* ${capFieldPtr}`); + this.emit(`br label %${doInsert}`); + + this.emit(`${doInsert}:`); + const dataPtrField2 = this.nextTemp(); + this.emit(`${dataPtrField2} = getelementptr inbounds %Set, %Set* ${setPtr}, i32 0, i32 0`); + const dataPtr2 = this.nextTemp(); + this.emit(`${dataPtr2} = load double*, double** ${dataPtrField2}`); + const insertPtr = this.nextTemp(); + this.emit(`${insertPtr} = getelementptr inbounds double, double* ${dataPtr2}, i32 ${currentSize}`); + this.emit(`store double ${dblValue}, double* ${insertPtr}`); const newSize = this.nextTemp(); this.emit(`${newSize} = add i32 ${currentSize}, 1`); this.emit(`store i32 ${newSize}, i32* ${sizeFieldPtr}`); + this.emit(`br label %${endLabel}`); - // Return the set (for chaining) + this.emit(`${endLabel}:`); return setPtr; } @@ -304,14 +375,98 @@ export class StringSetGenerator { const currentSize = this.nextTemp(); this.emit(`${currentSize} = load i32, i32* ${sizeFieldPtr}`); - const valueElemPtr = this.nextTemp(); - this.emit(`${valueElemPtr} = getelementptr inbounds i8*, i8** ${valuesPtr}, i32 ${currentSize}`); - this.emit(`store i8* ${valueValue}, i8** ${valueElemPtr}`); + const dedupLoop = this.nextLabel('strset_add_dedup'); + const dedupBody = this.nextLabel('strset_add_dedup_body'); + const dedupNext = this.nextLabel('strset_add_dedup_next'); + const alreadyExists = this.nextLabel('strset_add_exists'); + const dedupDone = this.nextLabel('strset_add_dedup_done'); + const resizeLabel = this.nextLabel('strset_add_resize'); + const doInsert = this.nextLabel('strset_add_insert'); + const endLabel = this.nextLabel('strset_add_end'); + + const idxReg = this.nextTemp(); + this.emit(`${idxReg} = alloca i32`); + this.emit(`store i32 0, i32* ${idxReg}`); + this.emit(`br label %${dedupLoop}`); + + this.emit(`${dedupLoop}:`); + const curIdx = this.nextTemp(); + this.emit(`${curIdx} = load i32, i32* ${idxReg}`); + const idxCond = this.nextTemp(); + this.emit(`${idxCond} = icmp slt i32 ${curIdx}, ${currentSize}`); + this.emit(`br i1 ${idxCond}, label %${dedupBody}, label %${dedupDone}`); + + this.emit(`${dedupBody}:`); + const elemPtr = this.nextTemp(); + this.emit(`${elemPtr} = getelementptr inbounds i8*, i8** ${valuesPtr}, i32 ${curIdx}`); + const elemVal = this.nextTemp(); + this.emit(`${elemVal} = load i8*, i8** ${elemPtr}`); + const cmpResult = this.nextTemp(); + this.emit(`${cmpResult} = call i32 @strcmp(i8* ${elemVal}, i8* ${valueValue})`); + const match = this.nextTemp(); + this.emit(`${match} = icmp eq i32 ${cmpResult}, 0`); + this.emit(`br i1 ${match}, label %${alreadyExists}, label %${dedupNext}`); + + this.emit(`${dedupNext}:`); + const nextIdx = this.nextTemp(); + this.emit(`${nextIdx} = add i32 ${curIdx}, 1`); + this.emit(`store i32 ${nextIdx}, i32* ${idxReg}`); + this.emit(`br label %${dedupLoop}`); + + this.emit(`${alreadyExists}:`); + this.emit(`br label %${endLabel}`); + this.emit(`${dedupDone}:`); + const capFieldPtr = this.nextTemp(); + this.emit(`${capFieldPtr} = getelementptr inbounds %StringSet, %StringSet* ${setPtr}, i32 0, i32 2`); + const currentCap = this.nextTemp(); + this.emit(`${currentCap} = load i32, i32* ${capFieldPtr}`); + const needResize = this.nextTemp(); + this.emit(`${needResize} = icmp eq i32 ${currentSize}, ${currentCap}`); + this.emit(`br i1 ${needResize}, label %${resizeLabel}, label %${doInsert}`); + + this.emit(`${resizeLabel}:`); + const isZero = this.nextTemp(); + this.emit(`${isZero} = icmp eq i32 ${currentCap}, 0`); + const doubled = this.nextTemp(); + this.emit(`${doubled} = mul i32 ${currentCap}, 2`); + const newCap = this.nextTemp(); + this.emit(`${newCap} = select i1 ${isZero}, i32 4, i32 ${doubled}`); + const newCapI64 = this.nextTemp(); + this.emit(`${newCapI64} = zext i32 ${newCap} to i64`); + const newMemSize = this.nextTemp(); + this.emit(`${newMemSize} = mul i64 ${newCapI64}, ${this.getPtrSize()}`); + const newMem = this.nextTemp(); + this.emit(`${newMem} = call i8* @GC_malloc(i64 ${newMemSize})`); + const newDataPtr = this.nextTemp(); + this.emit(`${newDataPtr} = bitcast i8* ${newMem} to i8**`); + const oldDataI8 = this.nextTemp(); + this.emit(`${oldDataI8} = bitcast i8** ${valuesPtr} to i8*`); + const newDataI8 = this.nextTemp(); + this.emit(`${newDataI8} = bitcast i8** ${newDataPtr} to i8*`); + const currentSizeI64 = this.nextTemp(); + this.emit(`${currentSizeI64} = zext i32 ${currentSize} to i64`); + const copySize = this.nextTemp(); + this.emit(`${copySize} = mul i64 ${currentSizeI64}, ${this.getPtrSize()}`); + this.emit(`call void @llvm.memcpy.p0i8.p0i8.i64(i8* ${newDataI8}, i8* ${oldDataI8}, i64 ${copySize}, i1 false)`); + this.emit(`store i8** ${newDataPtr}, i8*** ${valuesFieldPtr}`); + this.emit(`store i32 ${newCap}, i32* ${capFieldPtr}`); + this.emit(`br label %${doInsert}`); + + this.emit(`${doInsert}:`); + const dataPtrField2 = this.nextTemp(); + this.emit(`${dataPtrField2} = getelementptr inbounds %StringSet, %StringSet* ${setPtr}, i32 0, i32 0`); + const dataPtr2 = this.nextTemp(); + this.emit(`${dataPtr2} = load i8**, i8*** ${dataPtrField2}`); + const insertPtr = this.nextTemp(); + this.emit(`${insertPtr} = getelementptr inbounds i8*, i8** ${dataPtr2}, i32 ${currentSize}`); + this.emit(`store i8* ${valueValue}, i8** ${insertPtr}`); const newSize = this.nextTemp(); this.emit(`${newSize} = add i32 ${currentSize}, 1`); this.emit(`store i32 ${newSize}, i32* ${sizeFieldPtr}`); + this.emit(`br label %${endLabel}`); + this.emit(`${endLabel}:`); return setPtr; } From 6a5800b9efa0d0f9253fc4b89690397d6cd955db Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 19 Feb 2026 11:18:09 -0800 Subject: [PATCH 4/6] add set overflow test fixtures for numeric and string sets --- .../fixtures/data-structures/set-overflow.ts | 42 +++++++++++++++++++ .../data-structures/string-set-overflow.ts | 36 ++++++++++++++++ tests/test-fixtures.ts | 12 ++++++ 3 files changed, 90 insertions(+) create mode 100644 tests/fixtures/data-structures/set-overflow.ts create mode 100644 tests/fixtures/data-structures/string-set-overflow.ts diff --git a/tests/fixtures/data-structures/set-overflow.ts b/tests/fixtures/data-structures/set-overflow.ts new file mode 100644 index 00000000..024ab640 --- /dev/null +++ b/tests/fixtures/data-structures/set-overflow.ts @@ -0,0 +1,42 @@ +const s = new Set(); +s.add(1); +s.add(2); +s.add(3); +s.add(4); +s.add(5); +s.add(6); +s.add(7); +s.add(8); +s.add(9); +s.add(10); + +if (s.size !== 10) { + console.log("FAIL: expected size 10, got " + s.size.toString()); + process.exit(1); +} + +s.add(5); +s.add(10); +if (s.size !== 10) { + console.log("FAIL: dedup failed, expected size 10, got " + s.size.toString()); + process.exit(1); +} + +if (!s.has(1)) { + console.log("FAIL: missing 1"); + process.exit(1); +} +if (!s.has(5)) { + console.log("FAIL: missing 5"); + process.exit(1); +} +if (!s.has(10)) { + console.log("FAIL: missing 10"); + process.exit(1); +} +if (s.has(11)) { + console.log("FAIL: found 11 which was never added"); + process.exit(1); +} + +console.log("TEST_PASSED"); diff --git a/tests/fixtures/data-structures/string-set-overflow.ts b/tests/fixtures/data-structures/string-set-overflow.ts new file mode 100644 index 00000000..fbf58a8a --- /dev/null +++ b/tests/fixtures/data-structures/string-set-overflow.ts @@ -0,0 +1,36 @@ +const s = new Set(); +s.add("alpha"); +s.add("bravo"); +s.add("charlie"); +s.add("delta"); +s.add("echo"); +s.add("foxtrot"); +s.add("golf"); +s.add("hotel"); + +if (s.size !== 8) { + console.log("FAIL: expected size 8, got " + s.size.toString()); + process.exit(1); +} + +s.add("alpha"); +s.add("echo"); +if (s.size !== 8) { + console.log("FAIL: dedup failed, expected size 8, got " + s.size.toString()); + process.exit(1); +} + +if (!s.has("alpha")) { + console.log("FAIL: missing alpha"); + process.exit(1); +} +if (!s.has("hotel")) { + console.log("FAIL: missing hotel"); + process.exit(1); +} +if (s.has("india")) { + console.log("FAIL: found india which was never added"); + process.exit(1); +} + +console.log("TEST_PASSED"); diff --git a/tests/test-fixtures.ts b/tests/test-fixtures.ts index 1a39151f..cd3416da 100644 --- a/tests/test-fixtures.ts +++ b/tests/test-fixtures.ts @@ -502,6 +502,18 @@ export const testCases: TestCase[] = [ expectedExitCode: 1, description: 'Set with add/has operations should work' }, + { + name: 'set-overflow', + fixture: 'tests/fixtures/data-structures/set-overflow.ts', + expectTestPassed: true, + description: 'Set with >4 elements should grow buffer correctly' + }, + { + name: 'string-set-overflow', + fixture: 'tests/fixtures/data-structures/string-set-overflow.ts', + expectTestPassed: true, + description: 'StringSet with >4 elements should grow buffer correctly' + }, { name: 'set-type-args', fixture: 'tests/fixtures/collections/set-type-args.ts', From d950e1c77586477039d5807b0e4ce38c6126b105 Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 19 Feb 2026 11:20:04 -0800 Subject: [PATCH 5/6] add test fixtures for nested interface access and string += concat --- .../interfaces/nested-interface-access.ts | 16 ++++++++++++++++ tests/fixtures/strings/string-concat-assign.ts | 11 +++++++++++ tests/test-fixtures.ts | 12 ++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/fixtures/interfaces/nested-interface-access.ts create mode 100644 tests/fixtures/strings/string-concat-assign.ts diff --git a/tests/fixtures/interfaces/nested-interface-access.ts b/tests/fixtures/interfaces/nested-interface-access.ts new file mode 100644 index 00000000..29e1d921 --- /dev/null +++ b/tests/fixtures/interfaces/nested-interface-access.ts @@ -0,0 +1,16 @@ +interface Inner { + value: number; +} + +interface Outer { + inner: Inner; + name: string; +} + +const obj: Outer = { inner: { value: 42 }, name: "test" }; +const v = obj.inner.value; +if (v === 42 && obj.name === "test") { + console.log("TEST_PASSED"); +} else { + console.log("FAIL: expected 42/test, got " + v.toString() + "/" + obj.name); +} diff --git a/tests/fixtures/strings/string-concat-assign.ts b/tests/fixtures/strings/string-concat-assign.ts new file mode 100644 index 00000000..973d80f5 --- /dev/null +++ b/tests/fixtures/strings/string-concat-assign.ts @@ -0,0 +1,11 @@ +let s = ""; +let i = 0; +while (i < 50) { + s += "x"; + i = i + 1; +} +if (s.length === 50) { + console.log("TEST_PASSED"); +} else { + console.log("FAIL: expected length 50, got " + s.length.toString()); +} diff --git a/tests/test-fixtures.ts b/tests/test-fixtures.ts index cd3416da..ff58edd0 100644 --- a/tests/test-fixtures.ts +++ b/tests/test-fixtures.ts @@ -797,6 +797,12 @@ export const testCases: TestCase[] = [ expectTestPassed: true, description: 'Named and inline type assertions should produce identical results' }, + { + name: 'nested-interface-access', + fixture: 'tests/fixtures/interfaces/nested-interface-access.ts', + expectTestPassed: true, + description: 'Chained interface field access (a.b.c) should work' + }, { name: 'interface-array-mutation', fixture: 'tests/fixtures/arrays/interface-array-mutation.ts', @@ -815,6 +821,12 @@ export const testCases: TestCase[] = [ expectTestPassed: true, description: 'String builder with let re-declaration inside a loop should not segfault' }, + { + name: 'string-concat-assign', + fixture: 'tests/fixtures/strings/string-concat-assign.ts', + expectTestPassed: true, + description: 'String += accumulation in a loop should produce correct length' + }, { name: 'string-lastindexof', fixture: 'tests/fixtures/strings/string-lastindexof.ts', From 9661d33b59d1fef127e94fe78cfe99022c64414b Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Thu, 19 Feb 2026 11:20:44 -0800 Subject: [PATCH 6/6] add self-hosting tests to linux glibc ci job --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3e9c198..ca2f10dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,6 +207,10 @@ jobs: .build/chad build examples/hello.ts -o /tmp/hello2 /tmp/hello2 + - name: Run self-hosting tests + run: node --import tsx --test tests/self-hosting.test.ts + timeout-minutes: 15 + - name: Package release artifact run: | mkdir -p release/lib