
gBASIC
BASIC that grew up — money, dates and durations as real value kinds, a spreadsheet engine checked against 15,871 real Excel workbooks, and a whole business platform in one binary.
Download · Linux, x86-64 · 779 KBNo install step — extract it and run it. Other files and checksums.
gBASIC is a BASIC-family language for people who like BASIC's readability and
want real power underneath it. One self-contained interpreter in portable C11
— no runtime to install, no VM, no package manager, and nothing required to
build it but a C compiler, make and bison.
The syntax is the easy part to explain and the least interesting thing about it. What makes gBASIC worth a look is what it carries: money as an exact value kind, and on top of it double-entry accounting, loan servicing, deposits and credit analytics; a spreadsheet engine that edits real Excel files and has been measured against 15,871 of them; business-calendar scheduling — "third Thursday", payroll rolled off holidays, SLA clocks, timezones — as part of the standard vocabulary; a statistics library that runs from descriptive stats to survival models and causal inference; a declarative web server; and a stack for calling a model and letting it call back. Sixty-two pure-gBASIC libraries, and one binary.
- 97.38%of spreadsheet formula cells agree with Excel's own cached results, across 15,871 real workbooks — with no disagreement at all in 91.1% of them
- 0required dependencies. Every optional library degrades to a clean runtime error, never a build failure
- 145test suites — every one discovered by glob, so a gate cannot silently shrink
A taste
Assignment can carry a modifier — a small piece of behaviour attached at the point a value is set. It is how gBASIC gets money, dates and text transforms without ceremony:
program main(args)
price {USD} = 19.95
invoiced {date} = "2026-03-01 09:00:00"
name {uppered} = "ada lovelace"
print "price: " + price
print "with tax: " + (price * 1.08)
print "due: " + (invoiced + 45 days)
print "customer: " + name
end program
Prints 19.95, 21.55, 2026-04-15 09:00:00, ADA LOVELACE. Money, dates and durations are real value kinds — not floats and strings you have to remember the meaning of.
Money is a value kind, and business is the library
Money in gBASIC is an exact integer of minor units with four guard digits below them — never a float — and it carries its currency. That one decision is what the rest of the business platform is built on: a ledger that balances stays balanced, because there is no rounding error to sweep anywhere.
price {USD} = 19.95
print price * 1.08 ' 21.55
' Three people split a hundred dollars. The remainder goes out a cent at a
' time, so the parts add back up to exactly what was split.
bill {USD} = 100.00
print money.allocate(bill, 3) ' 33.34 33.33 33.33
' 250,000 at 6% a year, monthly, over 30 years.
loan {USD} = 250000.00
print finance.pmt(0.06 / 12, 360, loan) ' -1498.88
eur {EUR} = 10.00
print price + eur ' refused, by name
All 178 ISO 4217 currencies, each with its own minor unit — JPY has none,
KWD has three — from a table committed to the repository rather than read
from the build machine. Adding two currencies raises; comparing them
for equality answers false. Splitting three ways never loses a
cent nor invents one, and the payment figure is the one Excel and LibreOffice
give.
On that sit five more libraries, each with a cookbook the test suite
enforces: accounting (double-entry, where the accounting
equation is asserted after every posting rather than enforced
anywhere — it falls out of correct account typing, which is what makes it a
good test), lending (servicing as an auditable fold: replay the
events and the balance is explained, rather than stored and unexplainable),
deposits, credit (roll rates, vintage triangles,
charge-offs) and scoring (WOE binning, AUC, point scales).
These libraries are written the way they are because every defect they prevent is an ordinary-looking number. A ledger with the right amounts on the wrong side still balances. A roll rate computed over the loans still visible instead of the whole starting bucket is a perfectly plausible percentage. So the cookbooks put the wrong answer beside the right one, and the suites assert arithmetic rather than transcripts.
Spreadsheets it can actually be trusted with
Most spreadsheet libraries generate new files. gBASIC edits existing ones, which is a much harder guarantee: the reader keeps every part of a workbook, including the parts it does not understand, so saving never quietly destroys what it could not model.
It also does something unusual. An .xlsx stores the value Excel
computed for every formula the last time it calculated — which makes the file
its own test oracle. xlsx.check evaluates each formula and
compares:
program main(args)
wb = xlsx.open("quarterly.xlsx")
r = xlsx.check(wb, "Ledger")
print r.agree + " agree, " + r.disagree + " disagree, " + r.unsupported + " unsupported"
end program
You can ask, before trusting a single number, whether this engine and Excel agree on your workbook. Across a corpus of 15,871 real spreadsheets, 97.38% of judged formula cells agree and 91.1% of workbooks show no disagreement at all. What the engine cannot evaluate it refuses by name (an array formula, a reference into a workbook that isn't there), never answering with a plausible wrong number — which is why the rate is quoted over cells it actually judged rather than over all of them, since counting a refusal as a pass would flatter it.
On top of that sit three libraries that turn a messy sheet into something a
database can answer questions about: grid finds the real table
inside a report full of titles, subtotals and notes (and tells you how
confident it is), consolidate merges differently-shaped sources
onto one schema, and dbframe loads the result into SQLite with
column types inferred from every value rather than the first.
Calendars are a language feature
No mainstream language treats business-calendar work — the third Thursday, payroll that rolls off holidays, "respond within 4 business hours" — as a first-class part of its standard vocabulary. gBASIC does, and it starts with arithmetic that respects the calendar:
jan31 {date} = "2026-01-31"
print jan31 + 1 month ' 2026-02-28 — clamped, the accountant's rule
cal = dates.calendar({ holidays: [xmas], hours: { open: "9:00", close: "17:00" } })
' the board meets every third Thursday at 14:00
board = { every: "month", when: { nth: 3, weekday: "thursday" }, at: "14:00" }
meetings = dates.series(board, { from: jan1, through: dec31 }, cal)
' an SLA clock: nights, weekends and holidays excluded
due = dates.add_business_hours(ticket, 4 hours, cal)
' stored as civil time + zone, rendered anywhere — DST handled per occurrence
print to_zone(from_zone(m, "America/Chicago"), "Europe/Berlin")
A Friday-16:00 ticket with a 4-business-hour SLA is due Monday 12:00. A rule is a record — store it in a database, print it, send it to another process. Rules that no day satisfies answer unknown; questions with no honest answer (1 month > 30 days? a typo'd timezone?) are refused rather than guessed.
Calendars are data, so merging two people's calendars is a union of
constraints and "a day that works for everyone" is one call. On top sit
schedule.slots (appointment grids) and
schedule.layout (sessions packed into working days around an
immovable lunch — anything that fits nowhere is reported by name, never
silently dropped). The whole surface is a
12-recipe cookbook
whose every example the test suite runs.
Charts are strings
A chart in gBASIC is a string of SVG — no image library, no canvas object, and byte-for-byte deterministic, so charts are tested against goldens like everything else. Lines, scatter, area, bars, histograms, pies, correlation heatmaps and sparklines, straight off the frames the spreadsheet and statistics layers produce. Missing data draws as a gap, never a silent zero; a chart that would mislead — a stacked bar with negatives, a pie with an unknown share — is refused with an error that names the problem.
q = { quarter: [1, 2, 3, 4, 5, 6, 7, 8],
revenue: [310, 345, unknown, 420, 462, 501, 548, 610],
costs: [270, 280, 285, 300, 315, 330, 355, 370] }
s = chart.spec("line", q)
s = chart.x(s, "quarter")
s = chart.y(s, ["revenue", "costs"])
s = chart.title(s, "Drawn by gBASIC - the code above, verbatim")
s = chart.size(s, 560, 260)
s = chart.options(s, { markers: true })
print(chart.render(s))
The picture above is that program's output, pasted verbatim — note
the gap where the third quarter's revenue is unknown. The
11-recipe cookbook
is enforced by the test suite like the other two.
Ideas most BASICs don't have
Reactive watchers. Attach behaviour to values and it re-runs when they change — reactive programming with no framework and no wiring.
subtotal = 100.00
tax_rate = 0.085
watch pricing(subtotal, tax_rate)
total = subtotal * (1 + tax_rate)
end watch
print total ' 108.5
subtotal = 250.00
print total ' 271.25
unwatch pricing ' a named watcher is a first-class value with an off-switch
Naming a watcher binds a handle you can store, pass, and turn off —
re-declaring the same name replaces the old watcher instead of
stacking a second one, so setup code is safe to re-run, and
watchers() recovers any handle you lost. Nothing can fire with
its off-switch out of reach.
Policy-Based Inheritance. Compose objects by declaring what each field does on derivation — copied, shared, recomputed, or dropped — instead of building rigid class hierarchies.
account = {
owner (copy): "unassigned",
bank (link): "First National",
id (reset next_id): 0,
notes (exclude): "internal"
}
a = new account ' owner is its own; bank is shared;
b = new account ' id is recomputed; notes never appears
Shared-nothing actors. Real OS processes over
fork, with message passing, selective receive and death
monitoring. No shared memory means no data races.
function doubler(parent)
job = receive()
send(parent, job * 2)
end function
program main(args)
me = self()
w = spawn doubler(me)
send(w, 21)
print "worker replied: " + receive() ' 42
end program
Failure has a shape
A language earns trust by how it behaves when something goes wrong, and gBASIC's rule is that a failure is never silent. Errors are frame-scoped: a function decides its own error policy, and that decision cannot leak into the caller or be leaked into by it.
function parse_price(text)
on error goto next
n = number(text)
if error then
return unknown
end if
return n
end function
Prints 19.95, then unknown, then keeps running. on error goto next and on error goto LABEL are the whole vocabulary — there is no separate resume keyword to remember.
Beside it runs a second, quieter channel. A warning reports
something suspicious without stopping anything, and — unlike an error — its
volume is the caller's choice, not the callee's:
on warning print, ignore, stop, or
goto next. That suppression is what makes an aggressive warning
affordable, which is why gBASIC can afford to tell you that a return value was
discarded, or that a name you just assigned quietly shadows an outer one.
The same principle runs through the library surface. Operations that must
report failure as a value have non-raising twins —
try_decode for JSON you did not write,
xlsx.try_open so one bad workbook cannot end a batch of
thousands, process.which so asking "is this tool installed?"
never kills the program that asked.
A website is a block
The server is not a library you wire together — it is a declarative block, and it adds exactly one reserved word to the language:
server demo( port: 8080 )
get "/hello/{name}"( req )
return { body: "hello, " + req.params.name }
end get
end server
Route patterns are checked when the file loads, not when a request arrives: two routes that could never be told apart, a duplicate, a capture name used twice, a greedy segment that is not last — each is refused by name before the program runs. And because the block is data, the routes can be read and a request answered with no socket open at all, which is what makes a routing table testable:
print "routes declared: " + string(count(web.routes(demo)))
r = web.dispatch(demo, { id: 1, method: "GET", path: "/hello/ada", headers: {} })
print "answered with no socket open: " + r.body
Prints routes declared: 1 and answered with no socket open: hello, ada. Underneath sit TLS with SNI, streaming responses, a worker pool that reloads one worker at a time and never retires a healthy one on faith, and refusals for the request framings that carry smuggling attacks.
Calling a model, and letting it call back
tools is one declaration of what a model may
call, and both the schema the model is shown and the validation of what comes
back are derived from it — so they cannot disagree. The older shape had two
hand-written things: a JSON-Schema record and a function, with nothing
checking that the schema's id was the customer_id
the function read.
A tool that raises comes back as a result the model can read rather than ending the run, and tool bodies execute in a pre-spawned worker pool rather than on the event loop, where one slow call would freeze every other client.
agent is the piece that makes an approval gate possible: a
conversation in progress is a value, and the loop is a pure
step that performs no I/O and returns the actions its caller must perform.
That is what lets a run be stored between HTTP requests and an approval
arrive ten minutes later on a different one — a naive loop cannot wait for a
person on a single-threaded server. mcp publishes a toolset to
any Model Context Protocol client over stdio or HTTP through one dispatcher,
and consumes one the same way.
What a database estate says about itself
discovery reads the declared facts of a database estate — tables,
columns, keys, and the SQL of views and procedures — across several databases
at once, and traces where a column came from across hops.
Two reports both show a number called total_volume, they
disagree, and both are correct; what is wanted is where the derivations
parted. It infers nothing, deliberately: an inferred relationship is
the result of a search, and a search over a 500-table estate is some fifty
million candidate pairs where coincidences are a certainty.
nlq answers a question in English over such an estate, and is
built around the same finding. Hand a model twenty tables chosen badly and it
writes flawless SQL about the wrong ones — and the SQL runs, because
the tables it was given are real. Measured end to end, one question came back
36% short of its true answer by summing a warehouse table an ETL had already
filtered: valid SQL, real table, wrong question, nothing in the output saying
so. So a grounding carries its own search width, its near misses, and the
words it could not match, and it refuses rather than guesses when the catalog
cannot say which of two objects was meant.
Scored by an independently written answer key over nineteen questions — including deliberately ambiguous ones, and unanswerable ones where correctly declining is the right answer, with partial never folded into right — it scores 18 of 19 over a 127-object estate and 16 of 19 over a 517-object one. Grounding needs neither a model nor a database, which is exactly why that can be a gate rather than a demo.
An IDE, written in itself
gBASIC Studio is a desktop IDE for gBASIC — written in gBASIC, on GTK 4 through the GObject-Introspection bridge. It is the project's hardest dogfooding: an editor, a file browser, a run pipeline that executes a section in a child interpreter and keeps every result, a variable inspector, and state-only branches that replay the same source under different bindings.
Much of the platform on this page exists because Studio needed it and found it missing — the live child-process control, the source outliner, the atomic file replacement, the diagnostics stream. A language whose IDE is written in it cannot hide its own friction. Studio now lives in its own repository and consumes gBASIC the way any other application would, which is the more honest arrangement: it gets no privileged access to the platform it helped shape.
Batteries, optional by design
Every native module is detected at build time and compiles out cleanly when absent — a missing library turns one feature into a clear runtime error, never a broken build.
- Data — SQLite, PostgreSQL, ODBC (one connection string for SQL Server, MySQL, Oracle, DB2 and the rest), the xlsx engine, XML with a constant-memory streaming reader, JSON with a decode that reports failure as a value instead of raising.
- Network — an HTTP/HTTPS client, and a real HTTP server:
TLS with SNI, routing with typed captures, streaming responses, a process
worker pool with rolling reload, and request-smuggling defences. A whole
site is a
serverblock. - Time — precision-carrying datetimes, duration algebra that never blurs months into days, business calendars, date expressions, recurring schedules, SLA arithmetic, and timezone conversion over the system IANA database.
- Statistics — pure gBASIC: distributions, matrices, OLS and the GLM suite, clustering and PCA, time series through ARIMA and GARCH, survival analysis and Cox regression, meta-analysis, factor analysis, event studies and causal inference, plus data frames.
- Text — regular expressions as a first-class value kind, binary-safe Unicode strings, and a report-parsing spec language.
- Charts — pure-gBASIC SVG rendering: line, scatter, area, bar, histogram, pie, heatmap, sparklines; deterministic output, honest gaps, colorblind-safe palette.
- Desktop — a generic GObject-Introspection bridge, so any GObject library (GTK 4 first among them) is reachable without binding code, with constructors, a virtualizing table and a declarative reconciler layered over it. GUI code is testable without a display: widgets are built and interrogated, never shown.
- Shipping — an application becomes a
.debcarrying its own interpreter and standard library, built lean: a server build drops the desktop toolkit and takes the interpreter from 48 shared libraries to 10, while using the system's OpenSSL and SQLite rather than vendoring them. - Crypto and identity — hashing, HMAC, AES-GCM, Ed25519, PBKDF2 and scrypt key derivation, JWT and signed cookies over libcrypto; LDAP bind and search against a directory, where a directory that is down and a password that is wrong are deliberately different answers; and one-time passwords as a second factor, whose replay guard is structural rather than advisory.
- Mail — composing a message is pure gBASIC and needs no network, so the whole of it is testable without one; the native module owns only the wire.
Evidence, not adjectives
Output is compared byte-for-byte against committed goldens, and the claims on this page are the ones the test suite actually asserts. The spreadsheet engine is scored against Excel's own cached results rather than against itself. The build is exercised in clean containers with every optional dependency present, with none present, and installed to a prefix and run from there — because a conditional-compilation guard is invisible to the compiler you build with, and that is exactly how three defects once survived unnoticed.
Fifteen cookbooks are enforced. Every code block and every output block on those pages is owned by a file the test suite runs and compares byte for byte, so a page cannot drift from the product without a test going red. Spreadsheets, charts, dates & scheduling, money, finance, accounting, lending, deposits, credit, ODBC, and database discovery among them.
That harness pays for itself in a way worth naming. Writing the money
cookbook — an ordinary amortization schedule, not a stress case — surfaced a
silently wrong answer in money × scalar: a double's shortest
decimal can need more fractional places than a power of ten fits in an
int64, and every payment in a twelve-month schedule came out zero while a
360-month one worked. The unit tests had all used short scalars. Only
realistic worked examples produce a long one.
The full suite runs in CI on x86-64 Linux. riscv64 is a supported target, and the interpreter is also brought up on big-endian s390x — strings are byte-oriented and endian-independent, and a big-endian machine is how you find out whether that is actually true. macOS and Windows are not tested and no support for them is claimed.
Status
gBASIC 0.3.0 — an early release, and the version number is honest about that.
The release-candidate run was less about adding features than about closing the ways a program could fail quietly: a frame-scoped error model, a warning channel whose volume is the caller's choice, and a run of fixes for failures that reported nothing at all — a top-level file that stopped halfway and exited 0, a serializer that wrote what its own reader refused, child processes that outlived the interpreter that started them, two compound values that compared equal because the comparison had no branch for them. Since then the work has been the business platform on this page, and the AI stack under it.
It is still early and the language surface may still change; each
deliberate break is listed in the changelog with its migration. The known
limits are written down and executable — a test suite runs one
probe per documented limitation and goes red when one stops being true,
naming the entry to strike. That exists because five of fourteen entries
were once found to be false, each fixed by a shipped phase in the same
tree, and no amount of re-reading had caught it. Live today: no closures,
repeated string concatenation is quadratic, and timezone data comes from
the host system. (Scientific notation was on that list —
1e20 lexed as a duration, answered 0 seconds and
exited 0. It is a literal as of 0.3.0, and the duration path that swallowed
it now refuses.) See the
changelog,
and
DOGFOOD.md
for the friction log kept while building real software in it.
Licence
gBASIC is Apache-2.0 — all of it. Every file declares that in its own header.
Write gBASIC programs, embed the interpreter, build a product on any of it and ship it closed-source. Nothing here restricts you, and that includes the whole spreadsheet engine, the spreadsheet-to-database pipeline and the SEC/EDGAR analysis suite.
Ten libraries were AGPL until 0.3.0, with a commercial licence offered beside
them. That split is retired rather than adjusted: a gBASIC library is
source, so load combined it into the caller's own program with no
linking exception — which pointed the obligation at the one direction only the
caller can see. Contributions are open now too, inbound=outbound, with no
contributor agreement to sign.
Full map and the argument in LICENSING.md.
The book
gBASIC: Learning Programming teaches programming from nothing, at a prompt, to somebody who has never written a program. One program grows through most of it — a game that guesses an animal, asks what it got wrong, and gets better every time it loses — from three lines in Chapter 1 to a database by Chapter 9. Fourteen chapters, paperback, ISBN 9798177530765.
Every program printed in it is a real file, and every transcript is what
that file actually printed when it was run, captured by a program while
gBASIC ran it and captured again whenever gBASIC changes. The examples are
below, under Other files and checksums:
gbasic-learning-programming-examples.tar.gz, MIT licensed. The
transcripts are in it too, which is the point — if a chapter's output
does not match what you got, the file beside the program is what it printed
on the machine that wrote the book.
The book was written against gBASIC 0.3.0 and prints that archive's checksum on paper, so 0.3.0 stays downloadable here for as long as the edition is in print. Take a later version if one is offered — the book says so.
Errata
None reported yet. That is not a claim that there are none; it is a claim that nobody has told us. If you find something the book got wrong and it is not listed here, please say so — being told is the only way the list gets made, and a reader stuck because a page is wrong has no way of knowing it is the page.
Getting started
Download it and run it. There is no install step, no runtime to fetch, and nothing to set:
tar xzf gbasic-0.3.0-linux-x86_64.tar.gz
gbasic-0.3.0-linux-x86_64/bin/gbasic program.bas
Linux, x86-64, 779 KB. It is built against glibc 2.34 in a container on the oldest supported toolchain, so it runs on RHEL, Rocky and Alma 9, Ubuntu 22.04 LTS and later, Debian 12 and later, and anything newer — and the build refuses to publish an artifact whose floor has risen. It finds its own standard library wherever you unpack it.
That archive needs nothing at all, which is why it is the one to take first. It carries the language and 44 of the 65 standard libraries — everything that is pure gBASIC: dates, money, finance, accounting, lending, statistics, charts, frames and the rest. It also cannot open a database or fetch a URL.
If you want a database or the internet
Take gbasic-0.3.0-linux-x86_64-full.tar.gz instead — same
language, same libraries, plus SQLite, HTTP, the web client, email and the
crypto builtins all working. 52 of the 65 standard libraries
load in it.
It expects SQLite, libcurl, OpenSSL and zlib to be on the machine — present by default on ordinary desktop and server installs, and their sonames are the same across every distribution listed above, which is measured rather than assumed. If one is missing the binary will not start, so on something minimal take the first download.
Spreadsheets (xlsx), XML and password_hash are
deliberately in neither one. libxml2 and libxcrypt are the two
libraries whose soname is spelled differently on different distributions, so a
download linking them would fail to start somewhere. Those need a build from
source, which is a make away.
From source
A C11 compiler, make and bison is the whole list:
git clone https://github.com/solifugus/gbasic.git
cd gbasic
./tools/check-deps.sh # says what is optional and how to install it
make
./gbasic program.bas
Install it so gbasic is on your PATH and finds its standard library with no environment setup:
sudo make install # or: make install PREFIX=$HOME/.local
Optional features are auto-detected via pkg-config; whatever is
missing simply turns that feature into a clean runtime error. Start with the
tutorial,
or the
spreadsheet cookbook,
whose every example is a real file the test suite runs.
Downloads & installation
- gbasic-0.3.0-linux-x86_64-full.tar.gzsha256 24c9912147edb8dbac23a778fdb87cf0aca0146e21cdb5b90106260af294b383
- gbasic-0.3.0-linux-x86_64.tar.gzsha256 4a5bdf1ee22e7a019fa1065bee024af1c6ba1dc33a3565eab07a63dd1183a3d6
- gbasic-learning-programming-examples.tar.gzsha256 e94d9f20bd36c032de08cf1eb66c8d2ffc22cececba22f26afa1d368745dd20c