Scale measurements

These measurements show how compile, bundle, and run costs grow as ALGAL programs reuse helpers, and the size at which compilation refuses them.

A source project compiles to manifests, the JSON programs that both runtimes execute. A manifest names each child program by digest, so a bundle stores a shared helper once however many places call it. Every call still counts before a run: one compilation may expand to at most 1,024 manifest instances, 4,096 cells, 16,384 edges, and 64 MiB of manifest JSON, counting each call occurrence again. Building larger programs lists these limits beside the source limits. Browser task history is measured separately, because its verification cost grows with saved history rather than with program size.

What the numbers show

The tables report capacity and cost: how far each program grows before a limit refuses it, and what compiling, packing, and running it took on one machine. They say nothing about whether reusing programs improves later work; that is the open cumulative-skill experiment. The generated programs are synthetic shapes, each built to reach one limit.

Reproduce the measurements

From the repository root, with a release build of the native CLI:

sh
cargo build --release --locked -p algal
ALGAL_BIN=target/release/algal bun scripts/measure-source-scaling.ts \
  --markdown --out source-scaling.json
bun scripts/measure-history-scaling.ts --tasks 32 --title-length 120 \
  --out history.json

Each script writes a JSON record with every repetition and prints a table. Without ALGAL_BIN, the native times are omitted. The counts in the first three tables below do not depend on the machine, and bun test scripts/source-scaling.test.ts fails when one of them stops matching a fresh compile and run.

Example projects

Each example project entry point, compiled from its source files and run with its committed fixture:

  • Files and Source bytes: the source files the loader reads.
  • Modules: distinct compiled manifests, which is what a bundle stores.
  • Call sites: call and each expressions in the source.
  • Instances, Cells, Edges, and Manifest bytes: what the check before a run counts, with every call occurrence counted again.
  • Bundle bytes: the canonical bundle, which stores each module once.
  • Attempts: the compiler's upper bound on model executor attempts.
  • Depth: the call depth the root program must allow.
  • Steps, Work, and Run: the reference run's activated cells, work units, and complete or its error code.
  • ProgramFilesSource bytesModulesCall sitesInstancesCellsEdgesManifest bytesBundle bytesAttemptsDepthStepsWorkRun
    task-planning/main61,818667344213,10912,49703929,994complete
    task-planning/inspect_task31,324345253410,4468,41002252,671complete
    support-queue/main41,734456324313,23611,27503848,964complete
    typed-tasks/scores21,002212893,8584,14501141,603complete
    inbox/inbox251722317175,7514,48441255,408complete
    ratios/ratios2334212752,2372,524018837EXPR_FAILED

    The task planner's clamp helper is two of its seven instances and one of its six modules, so its bundle is smaller than the manifest bytes the check counts. An each call counts as one instance however many items it runs, so the planner's run activates 92 cells for three tasks while the check counts 34. ratios fails on purpose: its second item divides by zero.

    Programs that share helpers

    The measurement script also generates projects that call shared helpers from more and more places. It grows each one through 1, 2, 4, and more units until something refuses it, then bisects to the exact boundary. The table shows one unit and the largest size that compiles; the JSON record has every step.

  • table-fan-in: each unit is a section that calls a stage 12 times, and the stage reads 8 shared lookup tables that take no parameters. A table costs one cell, so this shape reaches the instance limit first.
  • planner-fan-in: each unit is a batch of 16 calls to the task planner's plan_task, whose five files are copied unchanged. It reaches the cell limit.
  • dense-scorecard: each unit calls a scorecard 4 times, and each of the scorecard's 18 metrics reads all 12 of its inputs. It reaches the edge limit.
  • record-orders: each unit is 4 desks of 3 calls to a check with 16 Order parameters, where Order has 32 fields of a 32-field Line record. Each record parameter's port carries the record's 47,846-byte schema, so the check's 3,795-byte source file compiles to a 770,939-byte manifest, and this shape reaches the byte limit.
  • import-chain: each file calls the next. It reaches the limit of eight import levels.
  • compact-dag: each file calls the next 24 times.
  • drafts: each unit is 8 calls to a helper that makes one model call. A root program may declare at most 64 executor attempts.
  • ProgramSizeFilesSource bytesModulesCall sitesInstancesCellsEdgesManifest bytesBundle bytesAttemptsDepthStepsWorkRun
    table-fan-in1112,943112111024614788,07717,4620324625,560complete
    table-fan-in9113,51111299822,1981,323788,25323,022031,024106,382BUDGET_EXHAUSTED
    planner-fan-in173,30772298519677202,26630,8640451955,697complete
    planner-fan-in773,7937286803,6214,7391,412,14837,500041,024109,810BUDGET_EXHAUSTED
    dense-scorecard133,5103561381,039166,98559,4340213819,664complete
    dense-scorecard1534,410319762,04215,5852,497,14368,952021,024146,349BUDGET_EXHAUSTED
    record-orders145,27148183107119,341,738797,34003310414,602complete
    record-orders745,6494141202,1584,97765,388,878801,432031,0241,349,157BUDGET_EXHAUSTED
    import-chain119210121672884002207complete
    import-chain991,47498926178,1558,96708262,663complete
    compact-dag119310121674886002207complete
    compact-dag333,5613486012,4022,376822,50235,781021,024106,877BUDGET_EXHAUSTED
    drafts131,0523910545117,6258,516825410,540complete
    drafts831,50131673418408137,20013,29164241882,968complete

    A run activates at most 1,024 cells, while the check accepts up to 4,096. Every row with 1,024 steps passed the check and then stopped at that step limit with BUDGET_EXHAUSTED; of the largest sizes that compile, only import-chain and drafts run to completion.

    Where compilation stops

    One unit past each boundary, compilation refuses the project. The check stops as soon as one count would pass its limit, before it resolves the rest of the expansion. Instances checked is how many manifest instances it accepted before refusing; Full expansion is the instance count of the complete expansion, computed once per distinct module without building it.

    ProgramSizeLimitMessageInstances checkedFull expansion
    table-fan-in101,024 instancesexpanded compilation count budget exceeded1,0241,091
    planner-fan-in84,096 cellsexpanded compilation count budget exceeded766777
    dense-scorecard1616,384 edgesexpanded compilation count budget exceeded7981
    record-orders864 MiB of manifest JSONexpanded compilation manifest byte budget exceeded124137
    import-chain108 import levelsimport depth exceeds 8: link_10.algalnonenot compiled
    compact-dag44,096 cellsexpanded compilation count budget exceeded1,00314,425
    compact-dag94,096 cellsexpanded compilation count budget exceeded944114,861,197,401
    drafts964 executor attempts72 explicit effects exceed max_agent_calls 64; the budget counts executor attempts, including retriesnonenot compiled

    Nine compact-dag files, 13,851 bytes of source, would expand to more than 114 billion instances; the check refuses them after accepting 944. The same message covers the instance, cell, and edge limits, so the Limit column names the count that crossed. import-chain and drafts stop earlier, in the source loader and the source compiler, before any manifest exists. The static depth limit of 64 cannot be reached from source, because source imports stop at eight levels.

    Timings

    Each time is the median of five repetitions after one warm-up, in milliseconds, with the range in parentheses. The run used an Apple M4 Max with 16 cores and 128 GB of memory, macOS 26.5.2, Bun 1.3.14, and a release build of the native CLI from the same commit. Other work shared the machine throughout, with load averages between 22 and 28 on its 16 cores, so the times are indicative only.

  • Source: load and compile the source files.
  • Check: the check before a run. For a refused program, the source and check times end at the refusal.
  • Pack: build and serialize the bundle.
  • Run: the TypeScript runtime in the same process, with scripted model answers.
  • Native: algal run, or algal check for a refused program, as a separate process after unpacking the bundle. It includes starting the process, reading the unpacked store, and printing the receipt, so compare it with other native times rather than with Run. Every native run matched the reference run's outcome, steps, and work, and every native check refused with the same code and message.
  • ProgramSizeSource msCheck msPack msRun msNative ms
    task-planning/main8.5 (7.8–10)1.7 (1.4–2.9)1.1 (0.9–2.0)6.3 (5.0–6.9)15 (13–16)
    task-planning/inspect_task4.7 (3.9–7.8)1.1 (0.8–1.8)0.7 (0.7–0.9)1.3 (1.2–1.4)12 (9.9–12)
    support-queue/main5.7 (5.2–8.8)1.4 (1.2–1.5)0.8 (0.8–1.2)3.9 (2.9–5.2)15 (14–18)
    typed-tasks/scores3.0 (2.1–5.8)0.4 (0.4–0.5)0.4 (0.3–0.5)1.2 (0.9–1.7)8.6 (8.3–11)
    inbox/inbox1.7 (1.5–4.8)0.5 (0.4–0.9)0.4 (0.4–0.4)1.4 (1.2–1.7)8.8 (8.4–9.6)
    ratios/ratios1.7 (1.2–2.4)0.3 (0.2–0.4)0.2 (0.2–0.2)0.6 (0.4–0.9)7.4 (7.1–8.6)
    table-fan-in112 (9.8–14)4.8 (4.3–5.7)2.2 (2.2–2.9)13 (12–17)51 (50–52)
    table-fan-in918 (15–24)33 (31–36)5.4 (5.3–6.4)66 (62–76)339 (307–347)
    planner-fan-in110 (8.6–11)8.2 (7.8–8.4)3.4 (3.3–3.8)19 (17–21)72 (71–75)
    planner-fan-in717 (14–17)47 (45–48)8.1 (7.3–9.7)73 (64–85)590 (475–619)
    dense-scorecard115 (12–16)9.7 (7.8–10)8.3 (7.8–9.3)15 (15–16)60 (58–69)
    dense-scorecard1550 (42–53)138 (104–169)34 (27–35)144 (122–154)592 (549–712)
    record-orders1115 (109–121)65 (59–67)41 (41–45)1,687 (1,546–2,119)2,036 (1,955–2,103)
    record-orders7594 (445–720)954 (647–1,117)76 (58–93)8,767 (6,293–10,124)10,391 (9,468–13,764)
    import-chain10.8 (0.5–1.0)0.1 (0.1–0.1)0.1 (0.1–0.1)0.1 (0.1–0.1)7.6 (6.3–8.7)
    import-chain95.3 (4.7–6.5)0.9 (0.8–1.0)0.9 (0.6–1.0)1.0 (1.0–1.1)14 (11–20)
    compact-dag11.3 (0.5–2.2)0.1 (0.1–0.1)0.1 (0.1–0.1)0.1 (0.1–0.2)30 (7.9–64)
    compact-dag316 (15–17)26 (25–26)16 (15–17)39 (37–42)296 (274–335)
    drafts12.9 (2.8–4.1)1.1 (1.0–1.3)0.9 (0.9–0.9)2.5 (2.3–3.3)14 (13–16)
    drafts85.4 (4.3–7.4)5.7 (5.4–6.9)3.0 (2.9–4.0)16 (15–21)59 (56–60)
    table-fan-in1012 (12–14)29 (28–34)--265 (247–270)
    planner-fan-in821 (21–24)73 (63–79)--562 (468–629)
    dense-scorecard1641 (37–47)95 (86–103)--602 (579–693)
    record-orders8245 (221–258)471 (452–523)--4,322 (4,011–4,780)
    import-chain105.4 (5.0–10)----
    compact-dag448 (45–52)53 (53–57)--450 (407–483)
    compact-dag9306 (283–592)55 (46–164)--474 (433–491)
    drafts94.3 (4.0–5.6)----

    Rows without pack and run times are the refusals from the previous table. Median check times stay under a second. The largest, record-orders at seven regions, checks 120 instances holding 65 MB of manifest JSON in 954 ms, and the refusal at eight regions takes 471 ms of checking. The nine-file compact-dag is refused after 55 ms of checking and 306 ms of source compilation. Run time follows the work a run does more than the program's size: each record-orders call checks and compares sixteen 48 KB orders, so its runs take seconds, while table-fan-in at nine sections reaches the 1,024-step limit in 66 ms.