Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Full migration table (when reading older docs that say `var inscope` or `<-` for
- `table[key]` (read or assign) is **safe** — do NOT wrap in `unsafe(...)`. Some legacy daslib code has `unsafe(tab[k])`; do not propagate that pattern
- **Move-assign table literal:** `tab <- { "k" => v }` works for both `var tab <- { ... }` declarations and `tab <- { ... }` reassignment to existing variables
- **Table comprehension move-assign:** `tab <- { for(x in range(5)); x => x*x }` — same move-assign rules apply
- **`table<T>` (one type param) is the set type** — value type elided. `var s : table<int>; s |> insert(5); key_exists(s, 5)`. Distinct from `table<K; V>` (the map form); both shapes coexist.

### Iterators and `each`

Expand Down
18 changes: 18 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,21 @@ Every `.das` benchmark file in this directory tree is listed below, grouped by s
| File | Description |
|---|---|
| `bench_v_ldu.das` | Fusion-engine `Op2At` array-indexed read at sizeof(T) ∈ {4,8,12,16} — int, int64, float3, float4. Used to compare DAS_FUSION=0 vs current `DAS_LDU_WORKHORSE` ladder vs `v_zero+memcpy(sizeof(CTYPE))` |

## sql/

3-mode comparison: `_sql` macro over `:memory:` SQLite vs pure in-memory `array<T>` LINQ, with the array form measured both in its naive (intermediate-materializing) and `_fold`-fused shapes. Mirrors the `tests/dasSQLITE/parity_check_*.das` pattern but oriented to throughput.

| Mode | Source | Form |
|---|---|---|
| `m1` | `:memory:` SQLite | `_sql` — compile-time SQL emission, work pushed to the engine |
| `m3` | pre-populated `array<Car>` | plain LINQ chain — materializes intermediate filter/sort arrays |
| `m3f` | pre-populated `array<Car>` | `_fold` from `daslib/linq_boost` — fuses the chain into a single pass, in-place where possible |

| File | Description |
|---|---|
| `_common.das` | Shared `Car` `[sql_table]` + `fixture_db` / `fixture_array` (not a benchmark) |
| `select_where.das` | Filter chain — `_where(_.price > 500)` over 10K rows. Modest asymmetry; m3 walks every row. |
| `select_where_order_take.das` | Filter + sort + limit — `_where \|> _order_by(_.price) \|> take(10)`. SQL ORDER BY + LIMIT bounds work; m3 sorts the full filtered set. |
| `count_aggregate.das` | Aggregate — `count()` after `_where` over 1M rows. SQL pushes `COUNT(*)` to the engine returning one row; m3 materializes the full filtered array then counts it; m3f fuses where+count into one pass. Highest-asymmetry chain in daslang's favor. |
| `indexed_lookup.das` | Indexed point lookup — `_where(_.id == K)` against the PRIMARY KEY over 1M rows. SQLite uses the PK b-tree (O(log n)); m3/m3f have no index (O(n) linear scan). Inverse-asymmetry: SQLite wins by ~1000×, illustrating where indexed storage earns its keep. |
35 changes: 35 additions & 0 deletions benchmarks/sql/_common.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
options gen2
options persistent_heap

require daslib/sql public
require daslib/linq_boost public
require sqlite/sqlite_boost public
require sqlite/sqlite_linq public
require dastest/testing_boost public
require daslib/fio public

[sql_table(name = "Cars")]
struct Car {
@sql_primary_key id : int
name : string
price : int
}

def public fixture_db(db : SqlRunner; n : int) {
db |> create_table(type<Car>)
var rows : array<Car>
rows |> resize(n)
for (i in range(n)) {
rows[i] = Car(id = i + 1, name = "Car{i}", price = (i * 37) % 1000)
}
db |> insert(rows)
}

def public fixture_array(n : int) : array<Car> {
var arr : array<Car>
arr |> resize(n)
for (i in range(n)) {
arr[i] = Car(id = i + 1, name = "Car{i}", price = (i * 37) % 1000)
}
return <- arr
}
61 changes: 61 additions & 0 deletions benchmarks/sql/count_aggregate.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
options gen2
options persistent_heap

require _common public

let THRESHOLD = 500

// SQL pushes COUNT(*) to the engine returning one row.
// m3 must materialize the full filtered array, then walk to count it.
// m3f folds where+count into a single fused pass (no intermediate array).
// Highest-asymmetry comparison among the three benchmark chains.

// --- m1: _sql over :memory: ---
def run_m1(b : B?; n : int) {
with_sqlite(":memory:") $(db) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let c = _sql(db |> select_from(type<Car>) |> _where(_.price > THRESHOLD) |> count())
if (c == 0) {
b->failNow()
}
}
}
}

// --- m3: array LINQ (materializes intermediate filter array) ---
def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let c = arr |> _where(_.price > THRESHOLD) |> count()
if (c == 0) {
b->failNow()
}
}
}

// --- m3f: array LINQ folded into a single fused pass ---
def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let c = _fold(each(arr)._where(_.price > THRESHOLD).count())
if (c == 0) {
b->failNow()
}
}
}

[benchmark]
def count_aggregate_1m_m1(b : B?) {
run_m1(b, 1000000)
}

[benchmark]
def count_aggregate_1m_m3(b : B?) {
run_m3(b, 1000000)
}

[benchmark]
def count_aggregate_1m_m3f(b : B?) {
run_m3f(b, 1000000)
}
62 changes: 62 additions & 0 deletions benchmarks/sql/indexed_lookup.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
options gen2
options persistent_heap

require _common public

// Indexed point-lookup: _where(_.id == K) where id is the PRIMARY KEY.
// SQLite uses the PK b-tree -> O(log n).
// m3/m3f have no index -> O(n) linear scan over the array.
// Inverse-asymmetry of count_aggregate: m1 wins by complexity-class margin.

// --- m1: _sql over :memory: ---
def run_m1(b : B?; n : int) {
with_sqlite(":memory:") $(db) {
fixture_db(db, n)
let key = n / 2
b |> run("m1_sql/{n}") {
let c = _sql(db |> select_from(type<Car>) |> _where(_.id == key) |> count())
if (c == 0) {
b->failNow()
}
}
}
}

// --- m3: array LINQ (linear scan) ---
def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
let key = n / 2
b |> run("m3_array/{n}") {
let c = arr |> _where(_.id == key) |> count()
if (c == 0) {
b->failNow()
}
}
}

// --- m3f: array LINQ folded into a single fused pass ---
def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
let key = n / 2
b |> run("m3f_array_fold/{n}") {
let c = _fold(each(arr)._where(_.id == key).count())
if (c == 0) {
b->failNow()
}
}
}

[benchmark]
def indexed_lookup_1m_m1(b : B?) {
run_m1(b, 1000000)
}

[benchmark]
def indexed_lookup_1m_m3(b : B?) {
run_m3(b, 1000000)
}

[benchmark]
def indexed_lookup_1m_m3f(b : B?) {
run_m3f(b, 1000000)
}
56 changes: 56 additions & 0 deletions benchmarks/sql/select_where.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
options gen2
options persistent_heap

require _common public

let THRESHOLD = 500

// --- m1: _sql over :memory: ---
def run_m1(b : B?; n : int) {
with_sqlite(":memory:") $(db) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type<Car>) |> _where(_.price > THRESHOLD))
if (length(rows) == 0) {
b->failNow()
}
}
}
}

// --- m3: array LINQ (materializing intermediate arrays) ---
def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _where(_.price > THRESHOLD))
if (length(rows) == 0) {
b->failNow()
}
}
}

// --- m3f: array LINQ folded into a single fused pass ---
def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr)._where(_.price > THRESHOLD).to_array())
if (length(rows) == 0) {
b->failNow()
}
}
}

[benchmark]
def select_where_10k_m1(b : B?) {
run_m1(b, 10000)
}

[benchmark]
def select_where_10k_m3(b : B?) {
run_m3(b, 10000)
}

[benchmark]
def select_where_10k_m3f(b : B?) {
run_m3f(b, 10000)
}
65 changes: 65 additions & 0 deletions benchmarks/sql/select_where_order_take.das
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
options gen2
options persistent_heap

require _common public

let THRESHOLD = 500
let TAKE_N = 10

// --- m1: _sql over :memory: ---
def run_m1(b : B?; n : int) {
with_sqlite(":memory:") $(db) {
fixture_db(db, n)
b |> run("m1_sql/{n}", n) {
let rows <- _sql(db |> select_from(type<Car>)
|> _where(_.price > THRESHOLD)
|> _order_by(_.price)
|> take(TAKE_N))
if (length(rows) == 0) {
b->failNow()
}
}
}
}

// --- m3: array LINQ (materializing intermediate arrays) ---
def run_m3(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3_array/{n}", n) {
let rows <- (arr |> _where(_.price > THRESHOLD)
|> _order_by(_.price)
|> take(TAKE_N))
if (length(rows) == 0) {
b->failNow()
}
}
}

// --- m3f: array LINQ folded into a single fused pass ---
def run_m3f(b : B?; n : int) {
let arr <- fixture_array(n)
b |> run("m3f_array_fold/{n}", n) {
let rows <- _fold(each(arr)._where(_.price > THRESHOLD)
._order_by(_.price)
.take(TAKE_N)
.to_array())
if (length(rows) == 0) {
b->failNow()
}
}
}

[benchmark]
def select_where_order_take_10k_m1(b : B?) {
run_m1(b, 10000)
}

[benchmark]
def select_where_order_take_10k_m3(b : B?) {
run_m3(b, 10000)
}

[benchmark]
def select_where_order_take_10k_m3f(b : B?) {
run_m3f(b, 10000)
}
37 changes: 37 additions & 0 deletions daslib/linq_boost.das
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,11 @@ var private g_foldSeq = [ // those are applied in order
calls = ["distinct", "order" ],
folder = @@fold_order_distinct
),
// where + count (single-pass count, no intermediate filter array)
FoldSequence(
calls = ["where_", "count"],
folder = @@fold_where_count
),
// select and where
FoldSequence(
calls = ["where_", "select" ],
Expand Down Expand Up @@ -1030,6 +1035,38 @@ def private fold_where(argIndex : int; var topValue : Expression?; var blk : Exp
return append_comprehension(argIndex, topValue, comprehension, blk, calls[0]._0.at)
}

[macro_function]
def private fold_where_count(argIndex : int; var topValue : Expression?; var blk : ExprBlock?; var calls : array<tuple<ExprCall?; LinqCall?>>) : Expression? {
//! folds `_where(p) |> count()` into a single-pass loop with the predicate inlined — no intermediate filter array, no block-call overhead
var eWhere = calls[0]._0
let srcName = "`source`{argIndex}`{eWhere.at.line}`{eWhere.at.column}"
let itName = "`it`{argIndex}`{eWhere.at.line}`{eWhere.at.column}"
let nName = "`n`{argIndex}`{eWhere.at.line}`{eWhere.at.column}"
var whereCond = fold_linq_cond(eWhere.arguments[1], itName)
var fusedCall : Expression? = qmacro(invoke($($i(srcName) : typedecl($e(topValue)) - const) {
var $i(nName) = 0
for ($i(itName) in $i(srcName)) {
if ($e(whereCond)) {
$i(nName) ++
}
}
return $i(nName)
}, $e(topValue)))
fusedCall.force_at(calls[0]._0.at)
fusedCall.force_generated(true)
let newArgName = "pass_{argIndex}"
blk.list |> emplace_new <| qmacro_expr() {
var $i(newArgName) = $e(fusedCall)
}
(blk.list.back() as ExprLet).variables[0].flags |= VariableFlags.can_shadow
if (argIndex != 0) {
blk.list |> emplace_new <| qmacro_expr() {
delete $e(topValue)
}
}
return qmacro($i(newArgName))
}

[macro_function]
def private fold_select(argIndex : int; var topValue : Expression?; var blk : ExprBlock?; var calls : array<tuple<ExprCall?; LinqCall?>>) : Expression? {
//! folds select into a single comprehension
Expand Down
30 changes: 30 additions & 0 deletions tests/linq/test_linq_fold.das
Original file line number Diff line number Diff line change
Expand Up @@ -724,3 +724,33 @@ def test_order_distinct(t : T?) {
}
}

[test]
def test_where_count_fold(t : T?) {
// Guards the where + count fold rule in linq_boost (fold_where_count).
// The fused loop emits a single counter pass with no intermediate filter array.
t |> run("where + count: half match") @(t : T?) {
let arr <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let c = _fold(each(arr)._where(_ > 5).count())
t |> equal(typeinfo typename(c), "int const")
t |> equal(5, c)
}

t |> run("where + count: zero matches") @(t : T?) {
let arr <- [1, 2, 3, 4, 5]
let c = _fold(each(arr)._where(_ > 999).count())
t |> equal(0, c)
}

t |> run("where + count: all match") @(t : T?) {
let arr <- [1, 2, 3, 4, 5]
let c = _fold(each(arr)._where(_ > 0).count())
t |> equal(5, c)
}

t |> run("where + count: empty source") @(t : T?) {
let arr : array<int>
let c = _fold(each(arr)._where(_ > 0).count())
t |> equal(0, c)
}
}

Loading
Loading