The features in earlier chapters fit together without a separate application language. A small program can start with top-level statements, grow into modules, and add checked resources or tasks where they help.
A complete word-count utility
This program reads lines until EOF and counts space-separated words without
assuming that SPLIT’s omitted separator means whitespace. Its input for the checked
example is two lines, red blue and red.
DIM counts AS DICT OF STRING TO INTEGER = DICT()
lines = 0
DO
IF LET line = ReadLine() THEN
lines += 1
AddWords(BYREF counts, line)
ELSE
EXIT DO
END IF
LOOP
PRINT "Lines: "; lines
FOR EACH word, count IN counts
PRINT word; ": "; count
NEXT
SUB AddWords(BYREF counts AS DICT OF STRING TO INTEGER, line AS STRING) FAILS StringError
FOR EACH word IN SPLIT(LOWER(line), " ")
IF word <> "" THEN
counts[word] = Get(counts, word, 0) + 1
END IF
NEXT
END SUBOutput:
Lines: 2
red: 2
blue: 1
The dictionary owns the counts; AddWords borrows it exclusively for updates and borrows the line for reading. Each loop’s bindings have clear scopes. Input EOF is normal optional absence. Dictionary iteration preserves insertion order, so the first encountered word is reported first.
This is deliberately a space-delimited counter. Punctuation remains part of a word, tabs are not separators and LOWER is not a complete linguistic tokenization policy. Extending it is an application decision: use Regex for a chosen word definition, Sorted(Keys(counts)) for alphabetical output, or a record for options.
Give a command-line tool typed options
TYPE Options FORMAT CommandSettings { Name="demo", Description="A small tool" }
Frames AS INTEGER = 10 FORMAT OptionSettings { Min=0, Max=1000, Help="Work limit" }
Verbose AS BOOLEAN = FALSE
END TYPE
TRY
SELECT TYPE ParseArguments(Options {})
CASE ArgumentResult.Help AS help
RETURN PrintArgumentHelp(help)
CASE ArgumentResult.Parsed AS parsed
options = ArgumentOptions(parsed) AS Options
PRINT options.Frames
END SELECT
CATCH usage AS ArgumentError
RETURN ReportArgumentError(usage)
END TRYRun the resulting program with --frames 25, --verbose or --help.
Help is a normal result; parse errors report usage and return status 2 through
the chosen helper. Neither helper exits behind the application’s back. Supported
record option types are scalar text/integer/real/Boolean and optional forms.
Use the dynamic Command API for subcommands, positionals and forwarding.
CommandLineParameters returns application arguments without argv[0]; the lower-level CommandLineArguments includes it. Explicit-list ParseArguments overloads are pure and expect the executable name already excluded.
Add useful logging
Logging records what a running application did and why. LogInfo("Started")
needs no setup: the default writes UTC timestamp, level and message to stderr
at the Info threshold. Levels are Trace, Debug, Info, Warn, Error and Fatal.
Fatal is severity only; it does not exit the process.
This example chooses deterministic output for testing:
config = DefaultLogConfig()
config.Console = LogConsole.StandardOutput
config.Format = "{level} {message}"
SetLogConfig(config)
LogDebug("Not enabled")
LogInfo("Processed {0} items", 3)
LogWarn("Request {0} took {1:.2f}ms", 42, 3.125)
LogFatal("Still returning normally")
PRINT "Alive"Numbered message fields share scalar f-string format modifiers; repeated fields reuse one computed argument. Double braces escape literal braces. Literal message patterns are checked at compilation; malformed dynamic patterns are reported at runtime. Layouts use named metadata: timestamp/date/time, level, message, file, line, function and logger. Severity calls supply source metadata automatically; explicit WriteLog does not fabricate caller metadata.
Defer expensive disabled messages
Ordinary argument expressions evaluate even when filtered. A zero-argument function value defers its computation until after filtering:
config = DefaultLogConfig()
config.Console = LogConsole.StandardOutput
config.Format = "{message}"
SetLogConfig(config)
number = 7
deferred = () => Compute(number)
number = 99
LogDebug("{0}", deferred)
SetLogLevel(LogLevel.Debug)
LogDebug("{0} twice: {0}", deferred)
FUNCTION Compute(value AS INTEGER) AS INTEGER
PRINT "Computing"
RETURN value * 2
END FUNCTIONThe disabled call does not print Computing. The enabled call invokes the function
once and produces 14 twice: 14. The closure captured 7 when constructed, so the
later assignment does not change it. Capture construction is still eager work;
reuse an existing closure in a hot loop. LogCallable supplies a function label
without invoking the function.
Deferred computation errors retain their original checked types. Severity calls report formatting/destination failures to stderr and continue other destinations. Use checked WriteLog when the caller needs LoggingError after destination attempts; LogArgument constructs typed choices for its heterogeneous formatting-value list. Configuration, Flush and Close also declare LoggingError. Cancellation remains cancellation.
Give services and destinations deliberate owners
CreateLogger(config) returns a scoped resource; CreateLogger(path) appends to a file with console output disabled. LogHandle(owner) gives a copyable access capability, not another owner. A handle does not keep a scoped logger alive or close it when dropped. Closed/stale handles are checked. GetLogConfig returns an independent value; SetLogConfig validates replacement before committing it.
SetDefaultLogger(TAKE owner) transfers ownership to program lifetime. Previously promoted services stay alive for existing handles even when the default selection changes. Program cleanup finishes children and ordinary locals before closing these services. This explicit shared service does not change independent-value semantics for ordinary application data.
LogRecord contains Timestamp, Level, Message, File, Line, FunctionName, Logger and Formatted. AddLogSink can register a stateless handler. For mutable or resource state, CreateLogSink takes an explicit independent owned context, a handler and an optional finishing callback:
TYPE CountedOutput
Count AS INTEGER
END TYPE
config = DefaultLogConfig()
config.Console = LogConsole.None
SetLogConfig(config)
sink = CreateLogSink(CountedOutput { Count=0 },
SUB(BYREF context AS CountedOutput, record AS LogRecord)
context.Count += 1
PRINT context.Count; ": "; record.Message
END SUB)
AddLogSink(TAKE sink)
LogInfo("first")
LogInfo("second")The sink owns CountedOutput and lends exclusive writable access to the handler. This avoids a mutable closure capture. Named resource contexts require TAKE. The optional finisher runs on checked Flush/Close and automatic service closure; ordinary cleanup then destroys the context. ClearLogSinks releases application sinks; the configured file destination is separate.
Sinks run synchronously in registration order and may suspend. Other tasks’ logger operations wait for exclusive service access. Same-service recursive logging reports failure rather than waiting on itself; cycles across different services still require normal deadlock avoidance. There is no logging thread or asynchronous record queue. Unused logging and unused file/formatting adapters are omitted from the executable. See the logging reference for all overloads, failure rules and recorded measurements.
Use the BASIL IDE
python3 tools/build_basic_ide.py
./basic-ide hello.bas
F1 shows help, F2 saves, F4 builds, F5 builds/runs and Ctrl+Space opens the compiler-owned standard-library browser. F6 navigates procedures/types; Ctrl-F searches and Ctrl-G goes to a line. F12 cycles themes. Ctrl-Q checks unsaved changes. The editor preserves UTF-8, common line endings and file permissions, and reports external save conflicts.
GDB-backed debugging starts with F7. F9 toggles breakpoints, F8 steps into, F10 steps over and F5 continues. GDB must be installed and tracing allowed. Aggregate inspection, task-aware stepping and complete source-level debugging of hosted/package code remain limited. Consult the IDE guide for current keys and boundaries.
Create a desktop application
The GTK package supplies opaque owned windows/widgets and queued application
events. Build it with python3 tools/build_gtk.py build/selfhost/stage3; declare
the dependency alias Gtk in your project. GTK and its development/system libraries
are separate host dependencies.
The following is a queued-event application skeleton, requiring that dependency and a graphical display:
Example requirements: GUI program requires a Gtk project dependency, GTK system libraries and a display.
IMPORT Gtk
window = Gtk.NewWindow("Hello", 480, 240)
app = Gtk.NewApplication(window)
pump = SPAWN Gtk.Pump(Gtk.EventSource(app))
button = Gtk.NewButton("Click me")
Gtk.SetChild(window, button)
clicked = Gtk.On(app, button, "clicked", 1)
Gtk.Present(window)
WHILE Gtk.IsRunning(app)
event = Gtk.NextEvent(app)
IF Gtk.Kind(event) = Gtk.EventKind.CloseRequested THEN
Gtk.Quit(app)
ELSEIF Gtk.Command(event) = 1 THEN
Gtk.SetLabel(button, "Hello, BASIC!")
END IF
WENDKeep the subscription owner alive while receiving events. The native signal only queues data; application work runs later in ordinary BASIL and can use state, checked failures and tasks. Direct C-stack handlers have stricter NOSUSPEND rules. Widgets own native references rather than independent snapshots of every property; a read-only borrow does not freeze GTK state.
The toolkit includes layout, menus, model-backed lists/tables/trees, dialogs, text buffers, drawing, clipboard and background files. Mixed document snapshots retain original image assets; bounded history supports grouped undo/redo and saved state. Image paste checks the captured revision/selection before committing, so a delayed request cannot overwrite newer edits. Full stylesheet owners and action subscriptions release their registrations on scope exit. The Notepad application and mixed-document editor show these pieces working together; the GTK package reference lists the supported API families.
Graphics and games
The raylib package provides an explicit rendering loop. Build the pinned package
using python3 tools/build_raylib.py build/selfhost/stage3 and declare Raylib as
a project dependency. The example requires a display and its native/system inputs:
Example requirements: graphics program requires a Raylib project dependency and a display.
IMPORT Raylib
Raylib.InitWindow(640, 400, "BASIC graphics")
Raylib.SetTargetFPS(60)
WHILE NOT Raylib.WindowShouldClose()
Raylib.BeginDrawing()
Raylib.ClearBackground(Raylib.Rgba(12, 18, 32))
Raylib.DrawCircle(320, 200, 70, Raylib.Rgba(64, 220, 190))
Raylib.EndDrawing()
WEND
Raylib.CloseWindow()Match drawing begin/end calls, check input each frame and use GetFrameTime for time-based motion. There is one coordinator-owned window. The binding includes procedural 3D cameras/meshes and checked draw states, with details in its reference . The complete Space Invaders project demonstrates options, packages and a game loop. Upstream raylib’s entire API is not automatically exposed by this binding.
Create images without a window
Canvas supplies native images, drawing, image codecs and fonts without importing a GUI library. It is useful for charts, thumbnails, reports and generated assets; it does not open a window or start an event loop. Its APIs are available directly.
image = NewCanvas(64, 32)
ClearCanvas(BYREF image, RGB(20, 30, 40))
FillRect(BYREF image, 2.0, 2.0, 12.0, 8.0, RGB(255, 0, 0))
DrawBitmapText(BYREF image, "BASIL", 2, 16, RGB(255, 255, 255))
copy = CloneCanvas(image)
SetPixel(BYREF copy, 0, 0, RGB(0, 255, 0))
PRINT GetPixel(image, 0, 0).Red
PRINT GetPixel(copy, 0, 0).Green
png = EncodePNG(image)
restored = DecodePNG(png)
PRINT CanvasWidth(restored)
PRINT CanvasHeight(restored)Coordinates start at the top left, with X increasing right and Y down. A new canvas is transparent black. Colors hold straight RGBA8 channels; RGB supplies opaque alpha, while RGBA accepts it explicitly. GetPixel/SetPixel check bounds; drawing operations clip. Geometry uses DOUBLE pixel-edge coordinates. A Canvas owns its pixel storage: ordinary arguments borrow, BYREF mutates, CloneCanvas makes a new owner, and TAKE transfers a named owner. CanvasBytes makes an independent byte snapshot. Assigning the resource does not silently copy it.
EncodePNG returns bytes. WriteFileAtomic(path, bytes) saves them, or use SaveImage(image, “picture.png”) for extension-selected output. LoadImage and DecodeImage inspect signatures. Direct codec calls select only the required format; a dynamic format choice retains its possible codecs. Literal/CONST choices can eliminate unused codec code and tables.
Paths, transforms and image processing
image = NewCanvas(16, 16)
path = NewPath()
MoveTo(BYREF path, 1.0, 1.0)
LineTo(BYREF path, 14.0, 1.0)
LineTo(BYREF path, 1.0, 14.0)
ClosePath(BYREF path)
FillPath(BYREF image, path, RGB(255, 0, 0))
small = ResizeCanvas(image, 8, 8, Sampling.Nearest)
PRINT CanvasWidth(small)
PRINT GetPixel(image, 2, 2).RedPaths support lines, quadratic/cubic curves, filling, strokes and dashed strokes. Stroke describes width, cap, join, miter limit and antialiasing. Fill rules select how overlapping contours count as inside. Antialiasing offers Off, Standard and High. DrawImage composites a borrowed source into a mutable destination, choosing BlendMode and encoded-sRGB or linear-light blending. For an overlapping copy inside the same owner use CopyWithinCanvas; separate read/write arguments naming the same owner conflict under ordinary borrowing rules.
ResizeCanvas, CropCanvas, Thumbnail, FlipCanvas, RotateCanvas and WarpCanvas return new owners. Nearest, Bilinear, Area, Bicubic and Lanczos3 sampling offer different quality/cost tradeoffs. Transform helpers compose/invert coordinate maps. AdjustedCanvas, grayscale/inversion, blur, sharpening, edge/emboss and custom 3x3 convolution support processing pipelines. Histogram, AverageColor, AnalyzeCanvas, AlphaBounds and ColorBounds inspect images. The Canvas reference groups these operations; the public API appendix gives their checked signatures and defaults.
Formats and animation
PNG/BMP are lossless image choices; JPEG supports quality-selected baseline output with an explicit background for transparency. GIF and WebP support still and animated images; APNG supplies animated PNG. WebP writing is lossless; reading also supports VP8 lossy input. These are defined format profiles, not a promise to accept every file variant. AVIF and TIFF enum members are reserved: capability queries return false. See the image-format reference for exact limits.
frame = NewCanvas(4, 4)
ClearCanvas(BYREF frame, RGB(255, 0, 0))
animation = NewAnimation(4, 4)
AppendAnimationFrame(BYREF animation, frame, 100, 1000)
SetPixel(BYREF frame, 0, 0, RGB(0, 255, 0))
savedFrame = AnimationFrame(animation, 0)
PRINT GetPixel(savedFrame, 0, 0).Red
PRINT CanEncodeImage(ImageFormat.AVIF)AppendAnimationFrame stores an independent snapshot, and AnimationFrame returns a fresh Canvas. Delays retain rational numerator/denominator timing. Use EncodeAnimation/SaveAnimation and DecodeAnimation/LoadAnimation for animation; still-image decoding rejects animations rather than dropping frames. There is no implicit playback task. Coalesced frames account for disposal/blending rules.
CanvasError reports Message, Offset and Format for malformed input, unsupported profiles and configured limits. File operations also declare IoError; helper error contracts remain visible. Choose pixel, encoded-size, frame and work limits for your inputs. They bound logical operations/storage, not total process memory or a wall-clock deadline. Normal scheduling, cancellation and deterministic cleanup apply. New-image operations leave their source unchanged on failure; arbitrary in-place drawing does not promise rollback.
Fonts and prepared text
DrawBitmapText uses a built-in ASCII font and needs no asset. For proportional or multilingual text, load a licensed static OpenType TrueType or CFF1 font. The following example requires your own font file; it is not a portable asset-free program.
Example requirements: requires a user supplied licensed font file.
font = LoadFont("fonts/body.ttf")
layout = CreateTextLayout(font, "Hello, café", TextLayoutOptions {})
image = NewCanvas(320, 80)
ClearCanvas(BYREF image, RGB(255, 255, 255))
DrawTextLayout(BYREF image, layout, 10.0, 10.0, RGB(0, 0, 0))
SaveImage(image, "greeting.png")Font and TextLayout are owned resources. A prepared layout owns its outlines and can outlive its source font; repeated drawing does not reshape the text. NewFontSet plus AddFont(BYREF fonts, TAKE font) creates an ordered fallback set. CreateTextLayout accepts either one Font or a FontSet. Measurement, line/glyph queries and hit testing refer back to original scalar positions.
Layout provides Unicode 17 grapheme boundaries, bidi ordering, normalization for shaping, whole-grapheme fallback, wrapping and alignment. GraphemeBoundaries, ResolveBidiLevels and BidiVisualOrder expose text analysis separately. This does not change ordinary STRING indexing into grapheme indexing. OpenType substitutions, positioning, kerning, marks and supported joining/reordering stages work without FreeType or HarfBuzz at runtime. Variable/CFF2/color fonts, WOFF and hint execution are unsupported. Missing glyphs use a replacement unless strict checking is requested. DiscoverSystemFonts/FindFont help locate fonts, but bundle fonts for repeatable output; discovery locations currently use Linux defaults. The font reference explains format, layout and resource limits.