Popup YouTube Video
Sheerpower Logo

A Guide for an AI when Vibe Coding


Sheerpower: A Guide for an AI when Vibe Coding

This guide is written for AI language models generating Sheerpower code. It focuses on the patterns most likely to produce incorrect output when models apply habits from C, Python, JavaScript, or Java — and shows the correct Sheerpower form alongside each trap.

Special note:

Generate only syntax that appears in this guide or in the official Sheerpower tutorials. When a statement, function name, parameter, or syntax rule cannot be confirmed from either source, do not invent a plausible-looking substitute. Instead, choose one of the following actions:

  • State the uncertainty in plain text.
    Example: I am not certain whether Sheerpower's string-search function is pos(), index(), or find(). Please confirm before I continue.
  • Request the relevant tutorial or reference material.
    Example: Before writing the file I/O section, I need to re-read the File I/O tutorial. Is it available?
  • Insert a clearly marked gap and continue around it.
    Example:
// UNVERIFIED: exact syntax for sorting a cluster array descending
// confirm against Cluster Sorting tutorial before running

A compile error on a marked gap is recoverable. A confabulated routine name buried in otherwise correct-looking code is not — it fails silently, erodes trust, and can take longer to track down than the original task.

Takeaway: Stop, mark, or ask — in that order. If a piece of Sheerpower syntax cannot be traced to this guide or the tutorials, that is the signal to pause, not proceed.

This document is structured around error patterns observed in generated code — not around language features.

The goal is simple: eliminate predictable errors in generated code before they reach execution.

Human readers will also find this a useful quick-reference for the rules that differ most from mainstream languages.

What this guide covers

  • Comment syntax — prefer //, but ! is okay for all new code.
  • Sheerpower routine calling syntax — no call prefix keyword, use named parameters.
  • All with arguments are read-only within the routine — compiler enforced.
  • Block terminators — the full list.
  • select case — always include case else.
  • Exception handling — attached vs. detached, retry vs. continue.
  • Loop control — do / repeat do / end do.
  • Variable suffixes — $, ?, and bare names.
  • sprintf$() format specifiers.
  • f$() formatted strings — text with values in [[]] slots instead of concatenation, and the one rule for routines.
  • Cluster access and iteration.
  • collect cluster before for each.
  • findrow() for fast single-row lookup.
  • The VIEW statement.
  • CGI web handler patterns.
  • Common built-in patterns such as element$(), elements(), size(), str$(), and pass.
  • What does not exist in Sheerpower.
  • Unique IDs — _GID$ special variable.
  • Exception handler discipline — when a handler earns its place.
  • Routine ordering — major to minor, callers before the routines they call.
  • The seven habits that make the next change easier — guards first, meaningful names, external data behind a boundary, states that cannot be invalid, decisions apart from actions, coded errors, focused changes.

The Ten Most Expensive Mistakes — Read This First

This guide is long. If you retain nothing else, retain these ten rules. They cover the mistakes AI code generators make most often, and every one is a hard rule: breaking it means a failed compile or a wrong-running program, not a style complaint. (Style preferences — naming, routine ordering, decorated headers — are marked as such in their own sections below.)

  1. No call, no positional arguments. Wrong: result = calc_tax(income, rate). Right: calc_tax with income, rate, returning tax result — or the function form with named arguments: result = calc_tax(income = income, rate = rate).
  2. Do not import keywords from other languages. No break, return, throw, catch, def, +=, --. See Keywords That Do Not Exist.
  3. Every block has its own terminator. end if (two words), end routine, end when — and a for loop closes with next varname, never end for.
  4. No escape sequences in strings. "\n" is a backslash and an n. Use chr$(10), or %text ... %end text for multi-line literals.
  5. Every select case needs case else. Without it, an unmatched value crashes at runtime.
  6. collect cluster before every for each. A for each without a prior collect is wrong even when it seems to work on a small test.
  7. Single-row lookup is findrow(), never a scan loop. It is an O(1) hash lookup on any field; findvalue() collapses lookup-and-read into one expression.
  8. No automatic true-or-false. Wrong: if count then. Right: if count <> 0 then; strings compare against "".
  9. Do not invent date helpers. Date arithmetic is days(), date$(), and seconds(); read the Date and Time Functions tutorial before writing any date/time code.
  10. Give an optional parameter a default, not a "missing" marker. Wrong: pass "" or -1 to mean "not supplied" and fix it up inside the routine (with parameters are read-only — that fix-up will not compile). Right: routine send_alert with message$, priority$ = "normal" and simply leave the parameter out when calling.

Sheerpower Source Code

To maximize portability, compatibility, and long-term maintainability, use only ASCII characters in all Sheerpower source code.

Sheerpower is case-insensitive. For consistency, write all code in lower-case and use snake_case for multi-word identifiers. Where ever possible, use meaningful variable names. This helps both code validation and code maintenance later on.

Calling Routines

This is the single most common error in AI-generated Sheerpower code. Sheerpower has no call prefix keyword when invoking routines. Routines are invoked by just their name, followed by with for inputs and returning for outputs. Sheerpower routine names cannot contain $ or ? characters. All with arguments are read-only within the routine — compiler enforced.

Problem: AI models trained on other languages generate call routine_name(args) or result = routine_name(args).

Solution: Use Sheerpower's named-parameter syntax. There is no call keyword and no positional argument list: the statement form binds by name with with / returning, and the function form (next section) takes name = value arguments in parentheses -- one leading value may stand alone, and binds the first parameter (calc_tax(1500, tax_rate = 0.12), or 1500 |> calc_tax(tax_rate = 0.12)).

Takeaway: Never write call. Never write result = routine_name(a, b) -- a second positional value is a compile error. Use the with / returning syntax below, or the function form with named arguments.

Wrong (do not generate)

call calculate_tax(income, rate) tax = calculate_tax(income, rate) calculate_tax(income, rate, tax)

Correct

// Full syntax calculate_tax with income=my_income, rate=tax_rate, returning tax tax_due // Without "=" calculate_tax with income my_income, rate tax_rate, returning tax tax_due // Implied -- variable names match parameter names exactly calculate_tax with income, rate, returning tax

Routine definition

routine calculate_tax with income, rate, returning tax tax = income * rate end routine

Parameters are read-only inside the routine by default. Do not attempt to assign to a with parameter — it is not allowed by the compiler.

A parameter's ending types it, as a variable's does: name$ takes text, count% a whole number, ok? true / false, rate# a real, and a bare name such as income adopts the type of the FIRST call compiled and keeps it for the whole program. The compiler checks every call against the header, wherever the header sits: text passed to a numeric parameter, or a number to a $ one, is a compile error at the call (Expected type string, got real), so convert at the call with str$() or val(). A parameter written values(*) takes a whole array, by reference, in either direction. Up to 16 with and 16 returning parameters; a cluster carries more.

Routines Are Also Functions

A routine with exactly one returning parameter can be called as a function inside any expression. The parentheses mark function use; the arguments are the same named bindings the statement form uses; and the function's value is the final content of the returning parameter. Nothing changes inside the routine — every existing single-returning routine is function-callable already.

routine calc_tax with amount, tax_rate, returning tax tax = amount * tax_rate end routine // statement form: calc_tax with amount = 1500, tax_rate = 0.12, returning tax my_tax // function form, same routine, untouched: print calc_tax(amount = 1500, tax_rate = 0.12) total_due = subtotal + calc_tax(amount = subtotal, tax_rate = 0.12)

The last character of the name tells Sheerpower what kind of value the call produces — the same spelling rule variables already follow. A plain name like calc_tax(...) produces a number. A name ending in $, like full_name$(...), produces text. % means a whole number and ? means a true/false value. That ending must agree with the routine's returning parameter — if it doesn't, the compiler stops and tells you — unless the returning parameter is a plain (flexible) name like tax, which can hand back whichever kind of value it holds. Routines may be defined after their call sites, as usual. A routine with zero returning parameters, or more than one, cannot be called as a function; the compiler says so. Neither can a once routine (routine name once): a function is evaluated at every call, but a once body runs only the first time — so the compiler stops that too, rather than let later calls silently receive the first call's result. Calling a routine as a function costs exactly the same as the statement form — both compile to identical code.

Routines with multiple returning values keep using the statement form, which handles up to 16 of them clearly.

Default Parameter Values

A with parameter can declare a default value. Calls that omit the parameter get the default; calls that pass it override it. The default is an expression, evaluated fresh at every call that omits the parameter — never once at definition time.

Problem: Models trained on Python expect defaults to be evaluated once at definition time, so they avoid live defaults and generate the "missing marker" dance instead: declare the parameter with no default, pass "" or -1, and patch the real value inside the routine. In Sheerpower that pattern is not just unnecessary — it does not compile, because with parameters are read-only.

Solution: Declare the default in the routine header. Per-call evaluation is the point: stamp$ = fulltime$ is freshly current on every call, and a module-variable default reads the variable's value at each call.

Takeaway: Python's mutable-default trap does not exist in Sheerpower. Never generate missing-marker-and-fix-up code for optional parameters — declare a default.

Wrong (do not generate)

// Python-style "missing marker" dance -- does not compile: // "Routine WITH parameters cannot be modified" routine log_event with event$, stamp$ if stamp$ = "" then stamp$ = fulltime$ print stamp$; " "; event$ end routine

Correct

routine log_event with event$, stamp$ = fulltime$ print stamp$; " "; event$ end routine log_event with event$ "backup started" // stamp$ is now log_event with event$ "restored", stamp$ "18-AUG-2026" // explicit override

Because arguments are named, there is no “defaults must come last” rule and no positional omission: a default may appear anywhere in the parameter list, and a call may omit any subset of defaulted parameters, in any order. A default may be a literal, a constant, a module variable, a built-in function, an expression combining them, or an earlier parameter in the same list (width = length); it may not call a routine. Declare the referenced parameter before the default that uses it: names in a default resolve left to right, and a name that only becomes a parameter later in the list silently binds the module variable of that name instead — a wrong value, not a compile error. returning parameters cannot take defaults. Function-form calls use defaults the same way: calc_tax(amount = 1000) applies a declared tax_rate default.

A parameter with no default is required — omitting it is a compile-time error at the call site (Parameter RATE -- was not passed and has no default value). Do not treat a parameter as optional unless its declaration shows a default.

Block Terminators — the Complete List

Every block in Sheerpower has an explicit named terminator. Missing or mismatching a terminator is a common AI error, especially when generating nested structures.

Problem: AI models omit terminators, use wrong keywords, or use braces from C-style habits.

Solution: Most blocks close with a two-word or three-word end ___ keyword — no braces.

Efficiency: Sheerpower's compiler reports the exact line of a missing terminator, making errors easy to find.

Takeaway: After generating any block, verify that its closing keyword appears in the list below.
end routine end handler end when end collect end do // one time loop loop // endless loop, use while or until or exit do, as needed end if end select end add

There are no braces. There is no endif as a single word — it must be written end if with a space.

for loops — both numeric and for each — close with next varname, not an end keyword.

// Numeric for loop for i = 1 to 10 print i next i // closes the for loop // For each over a cluster array for each orders print orders->customer$ next orders // closes the for each loop -- NOT "end for"

Nested structure example — correct terminators

routine process_orders with orders for each orders if orders->amount > 1000 then select case orders->region$ case "North" print "North: "; orders->amount case else print "Other: "; orders->amount end select end if next orders end routine

select case Requires case else

When generating a select case block, always include case else. If no case matches and there is no case else, the program can fail at runtime.

Problem: AI models often generate select case blocks with only the expected cases. This is fragile because real data can contain unexpected values.

Solution: Always include case else as the fallback branch.

Efficiency: A fallback branch makes the control flow explicit and prevents unexpected values from becoming runtime failures.

Takeaway: Every generated select case block should include case else.

Wrong

select case status$ case "open" print "Open" case "closed" print "Closed" end select

Correct

select case status$ case "open" print "Open" case "closed" print "Closed" case else print "Unknown status: "; status$ end select

Use VAL() as the Primary Way to Parse Numbers From Text

In Sheerpower, the primary way to extract numbers from text is VAL(). When numeric data appears inside a larger string, do not assume the text must be split apart first, and do not invent manual parsing helpers unless they are truly necessary.

Use VAL(string$) when the entire string should be a number. Use VAL(string$, Nth) when the string may contain embedded numbers and you want the first, second, third, or later numeric value. This makes VAL() the standard tool for parsing values from user input, reports, logs, messages, and mixed text.

Wrong (do not generate)

// hand-rolled digit scanning -- the pattern models import from // other languages. Do not generate any of this: qty = 0 for i = 1 to len(a$) c$ = mid$(a$, i, 1) if c$ >= "0" and c$ <= "9" then qty = qty * 10 + val(c$) next i

Correct

Given the string "I need 5 widgets at $12.99 each", extract the numbers directly:

a$ = "I need 5 widgets at $12.99 each" qty = val(a$, 1) price = val(a$, 2) print qty, price

This extracts 5 and 12.99 without any manual parsing or string splitting.

If parsing may fail, check _integer immediately after the call. A value of 0 means no valid number was found. When needed, use the optional parsing options to handle separators, ignored characters, negative formats such as parentheses, or European number conventions.

In short: when an AI needs to get a number out of text in Sheerpower, it should think of VAL() first.

Exception Handling

Sheerpower has two forms of exception handler. The words retry and continue mean something specific here and must not be confused with loop control.

Attached handler (most common)

when exception in // protected code here x = 10 / y use if extype = exceptiontype("divby0") then y = 1 retry // re-executes the failing statement else print "Error: "; extext$ continue // resumes at the statement AFTER the one that failed end if end when

Detached handler

when exception use fix_divide_error average = total / count end when print "Average: "; average handler fix_divide_error average = 0 continue end handler
Problem: AI models confuse retry and continue, or use throw / raise / except from other languages.

Solution: Use only the Sheerpower keywords shown above. To raise an exception deliberately, use cause exception.

Efficiency: Sheerpower's stackless VM rewinds call depth on exception at near-zero cost, so exception handlers are not expensive.

Takeaway: retry = re-run the failing statement after fixing the cause. continue = skip the failing statement and move on. Never write throw, raise, except, or catch.

Exception introspection variables

_error // TRUE if an exception occurred extype // numeric exception type extext$ // plain-text description of the exception exlabel$ // source location where the exception occurred (MAIN.13) exline // that line as a number (13); 0 before any exception systext$ // OS-level error description // Compile-time constant folding -- zero runtime cost when literal string used: exceptiontype("divby0") // returns 3001 exceptiontype("filenotfound") // returns 7110

Causing an exception deliberately

// cause exception takes a numeric exception number if quantity < 1 then cause exception 1001 if quantity < 1 then cause exception exceptiontype("illnum")

Loop Control

Sheerpower's main loop construct is do / end do. The keyword that restarts the loop from the top is repeat do — not continue. The keyword that exits is exit do.

Problem: AI models write continue inside a loop intending to restart the iteration. In Sheerpower, continue is an exception-handler keyword only.

Solution: Use repeat do to restart a do loop. Use exit do to break out.

Efficiency: repeat do is explicit about intent — there is no ambiguity about which loop is being restarted in nested structures.

Takeaway: Inside a loop, never write continue. Write repeat do to cycle, exit do to leave.

Wrong (do not generate)

do input "Enter a positive number": n if n <= 0 then continue // WRONG -- continue is not loop control print n * 2 end do

Correct

do input "Enter a positive number": n if n <= 0 then print "Must be positive. Try again." repeat do // restart the loop end if print n * 2 end do

For loops and for-each

// Numeric for loop for i = 1 to 10 print i next i // Step for i = 10 to 1 step -1 print i next i // Open-ended for loop -- no upper bound; use "exit for" to leave for idx = 1 VIEW token$ INTO data$, PIECE ",", MATCH idx if token$ = '' then exit for print token$ next idx // For each over a cluster array for each orders print orders->customer$; ": "; orders->amount next orders // Exit a for loop early for i = 1 to 100 if results(i) = "done" then exit for next i

Loop control keywords — complete list

Problem: AI models use continue inside for loops to skip to the next iteration or break to exit. Neither keyword exists for loop control in Sheerpower.

Solution: Use the keywords below. Each one is specific to its loop type.

Efficiency: Explicit keywords make it unambiguous which loop is being controlled in nested structures.

Takeaway: iterate for skips to the next for iteration. repeat do restarts a do loop. Never use continue or break for loop control.
// DO loop control repeat do // skip remaining body, restart from top of do loop exit do // exit the do loop entirely // FOR loop control iterate for // skip remaining body, advance to next for iteration exit for // exit the for loop entirely

Early exit from a routine

There is no return statement in Sheerpower. To exit a routine before reaching its end, use exit routine.

routine validate_amount with amount, returning ok? ok? = false if amount <= 0 then exit routine // early exit -- ok? stays false if amount > 1_000_000 then exit routine ok? = true end routine

Variable Naming and Type Suffixes

Sheerpower uses visual suffixes to declare variable type automatically. These are part of the variable name, not operators.

name$ // STRING -- ends with $ is_active? // BOOLEAN -- ends with ? total // REAL -- no suffix (default numeric type) count // REAL -- REAL holds integers accurately too
Problem: AI models omit the ? suffix on boolean variables, generating is_active = true instead of is_active? = true.

Solution: Any variable holding a boolean value must end with ? when declared by suffix. Without the suffix, Sheerpower creates a REAL variable and true stores as 1.

Efficiency: Suffix-based typing means no var, let, int, or float declarations needed for most cases.

Takeaway: Check every boolean variable name for the ? suffix. Check every string variable name for the $ suffix.

Explicit declarations when needed

declare string shipping_address // explicit string, no $ suffix needed declare real salary // explicit real declare boolean is_manager // explicit boolean, no ? suffix needed declare dynamic x // type determined at first use

Constants

const max_retries = 5 const app_name$ = "OrderSystem"
Problem: AI models write const pi = 3.14159 (or maxnum, true, date$) out of habit.

Solution: pi is built in — use it as is. A constant or variable named after a built-in value or a system variable (_integer, _extracted) is a compile error: "pi is built into Sheerpower -- a constant cannot take its name; choose another (my_pi)".

String Literals — No Escape Sequences

Sheerpower string literals are treated as raw text. There are no escape sequences inside strings.

Problem: AI models trained on C, Python, or JavaScript generate escape sequences such as \n, \t, or \\, or double-escape backslashes unnecessarily.

Solution: In Sheerpower, a string stores exactly the characters written. A backslash is just a backslash. There are no escape rules to apply.

Efficiency: This eliminates an entire class of bugs related to incorrect escaping and double-escaping across systems.

Takeaway: What you type is what is stored. Do not insert extra backslashes. Do not interpret escape sequences.

Example

a$ = "\" // a$ contains exactly one character: "\" b$ = "\n" // contains two characters: "\" and "n"

Do not apply escape rules from other languages. Write exactly the characters you want stored in the string.

Multi-Line String Literals — %text

For a string spanning several lines — HTML, SQL, help text, test data — use a %text block. Everything between %text and %end text is one string literal, exactly as typed, with lines joined by CR+LF.

Wrong (do not generate)

h$ = """multi line""" // Python syntax -- does not exist h$ = `multi line` // JavaScript syntax -- does not exist h$ = "multi" + chr$(13) + chr$(10) + "line" // works, but noisy

Correct

h$ = %text <h1>Report</h1> <p>Generated nightly.</p> %end text

The body is raw: comment indicators such as // and !, quotation marks, and indentation are all part of the string. %text must be the last thing on its line; %end text ends the block. Add the trim option — %text trim — to remove the indentation the lines share, so the block can be indented to match the surrounding code. One literal holds up to 16K; concatenate blocks for more.

Building Text with Values — f$()

Most text a program builds is a sentence with values in it: a message, a log line, a label. Do not assemble it from pieces with + and str$(); write the sentence and put each value in a [[]] slot. A slot holds any expression — a variable, arithmetic, a function call, a whole array — and is evaluated when f$() runs. A number renders as str$() writes it (no padding); to choose decimals, commas or width, put a sprintf$() inside the slot.

Wrong (do not generate)

msg$ = "Order " + str$(order_id) + " for " + name$ + ": " + str$(total) + " due" print "Hello " + name$ + ", you have " + str$(count) + " items" msg$ = f"Order {order_id} for {name$}" // Python f-string syntax -- does not exist msg$ = `Order ${order_id}` // JavaScript template -- does not exist

Correct

msg$ = f$("Order [[order_id]] for [[name$]]: [[sprintf$('%.2m',]] due") print f$("Hello [[name$]], you have [[sprintf$('%]]") print f$("scores: [[scores]], best [[stats$max(scores)]]") // a whole array renders as print shows it

Rules that matter for generated code:

  • Inside a routine, a slot sees the routine's own names — its with parameters and private variables — and the module variables too, exactly as the routine's own statements do.
  • A name that nothing in the program assigns raises a catchable UNKNOWNVAR exception with the identifier in _string$, the same rule as eval() — a misspelled slot is loud, not silent.
  • Keep the template a literal, or a variable set once: the slots are compiled on first use and cached by the template. A template assembled with + on every call is a new string every call and recompiles every call.
  • Every [[]] (else a catchable BADFORMAT); brackets and quoted text inside a slot are fine, so [[name$[1:3]]] and [["a]]b"]] both work; a single [ is plain text.
  • Cost: cheaper than the equivalent sprintf$(), and the fastest way to place values in text. Big literals — HTML, SQL, %text blocks — cost nothing to assign or pass: the runtime shares a literal's bytes rather than copying them, so write large templates as literals, not as strings assembled at run time.

Date and Time Functions

Do not invent date helpers — Sheerpower already has them. Use the built-in date and time functions for all date/time work, and do not assume syntax from any other language. Before writing any code that touches dates, times, or durations, read the Date and Time Functions Tutorial.

Wrong (do not generate)

date_diff = end_date$ - start_date$ // strings do not subtract tomorrow$ = dateadd("d", 1, date$) // invented helper print format_duration(elapsed) // invented helper

Correct

days_between = days(end_date$) - days(start_date$) // date math in days tomorrow$ = date$(days(date$) + 1) // YYYYMMDD print date$(days(date$) + 30, 3) // e.g. 18-Sep-2026

One catch worth knowing on top of the rule: seconds() requires a full datetime string, not just a date. A variable containing only YYYY-MM-DD will cause an illegal time format error.

The "90% IEEE Trap" and Exact REALs

Problem: In most mainstream languages (Python, Java, JavaScript, C++), numeric calculations use IEEE 754 binary floating-point. These values cannot exactly represent many decimal numbers. This leads to small but visible errors in results (for example, 0.1 + 0.2 = 0.30000000000000004).

Because of this, AI-generated code often adds round(), floor(), or similar adjustments after calculations to "correct" the result.

Sheerpower Difference: Sheerpower uses Exact REAL values stored in a decimal-accurate format. Decimal values are represented exactly, so calculations produce correct results without adjustment.

Instruction for AI:
Do not add rounding to compensate for floating-point error. In Sheerpower, the result of a calculation is already correct. For example, 0.1 + 0.2 evaluates exactly to 0.3.

Business Logic Only:
Apply rounding only when it is explicitly required by the business rules, for example, "round tax to the nearest cent." Do not round as a defensive habit.

Trust the VM:
Sheerpower's Virtual Machine is designed for precise decimal arithmetic. You can rely on the direct result of any numeric expression.

Range and rounding facts (so you do not compensate for them):
Whole numbers are exact to 54 digits and promote automatically beyond that — do not add BigDecimal-style ceremony, overflow guards, or string-based big-number workarounds. Division rounds half-to-even at 16 fraction digits by language rule (1/3 is sixteen threes) — do not hand-round quotients. Equality with = is exact and safe — do not generate tolerance-comparison patterns like abs(a - b) < epsilon unless the values came from a C-double source such as trig or logs. One more: the exact range is an ABSOLUTE grid with 16 digits after the decimal point, so relative precision falls for very small magnitudes (0.0000000001 / 3 keeps six significant digits). For quantities that live at tiny scales, scale the UNITS — store cents rather than fractional dollars, micrograms rather than fractional grams — the standard practice in every fixed-point system.

Takeaway:
If you are about to write round(total, 2) just to ensure cents are correct, stop. In Sheerpower, the cents are already correct.

Arrays — Whole-Array Operations Instead of Loops

An AI trained on C-family languages writes an index loop for everything. In Sheerpower an array is a value: most of those loops are one statement, and the compiler does the work in native code. Arrays are 1-origin; x(i) is an element (parentheses -- square brackets are slices and gathers); dim x(*) is expandable and x(*) = v appends; a whole-array result needs a declared array target, never a plain variable.

dim scores(5) fill scores with 90, 85, 77, 60, 95 print scores // 90 85 77 60 95 print size(scores); stats$mean(scores); stats$max(scores) // 5 81.4 95 dim names$(*) // expandable: appends grow it names$(*) = 'ada' names$(*) = 'bob' fill names$(*) with 'cy', 'dee' // append a list print size(names$); ' '; join$(names$, ', ') // 4 ada, bob, cy, dee dim passed(*) passed = filter(scores, scores >= 80) // the elements a condition selects print passed // 90 85 95 print join$(sort(scores, descending: true)) // 95,90,85,77,60 print join$(scores * 2) // element-wise arithmetic: 180,170,154,120,190 print join$(ucase$(names$), '|') // string functions map: ADA|BOB|CY|DEE dim top(*) top = scores[1:3] // a slice: 90,85,77 scores(scores < 70) = 70 // a masked store: every element under 70 becomes 70 print stats$sum(scores > 80) // a comparison is a 1/0 array: how many over 80 -> 3 dim v(*) v = seq(0, 1, 0.25) // 0,0.25,0.5,0.75,1 -- exact decimals dim words$(*) words$ = split('red,green,blue') // text to a string array; join$() back total = total_of(values = scores) // an array parameter: by reference, nothing copied
routine total_of with values(*), returning total total = stats$sum(values) end routine

Wrong (do not generate): for i = 0 to n - 1 (arrays start at 1), x[i] for an element (that is a gather -- use x(i)), x.length / len(x) for an array (size(x)), x.append(v) (x(*) = v), a loop that sums, filters, sorts or uppercases element by element (stats$sum(x), filter(), sort(), ucase$(x$)). For a table of related columns use a cluster (next section), not parallel arrays. The full surface -- masks, where(), unique(), isin(), 2-D arrays, matmul() / solve(), axis: reductions -- is on the Arrays and Array Math pages.

Every n-th element, and tables of flags. A loop that writes the same value to every n-th element is one statement: fill x(start step n) with v, or the slice store x[start:*:n] = v. A ? array stores one bit per element (500 million flags take 62.5 MB), fill flags? with true sets them all, and stats$sum(flags?) counts the true ones. When one plain value is written across a large boolean, REAL or INT array, the engine spreads the work over the processor's cores.

dim flags?(50) // one bit per flag fill flags? with true fill flags?(4 step 3) with false // 4, 7, 10, ... 49 flags?[5:*:10] = false // 5, 15, 25, 35, 45 print stats$sum(flags?) // how many are still true: 30

Wrong (do not generate): for i = start to last step n / flags?(i) = false / next i to write one value (more than ten times slower on a large table), or an integer array with hand-made bit masks for a table of flags.

Clusters — Access and Iteration

Clusters are Sheerpower's primary structured data type. The field-access operator is ->.

Scalar cluster (single record)

cluster employee: name$, age, salary, position$ employee->name$ = "Maria Santos" employee->age = 34 employee->salary = 62_000 employee->position$ = "Senior Analyst" print employee->name$; " earns "; employee->salary

Cluster array (adding multiple rows)

cluster orders: customer$, amount, region$ add cluster orders orders->customer$ = "Acme Corp" orders->amount = 4500 orders->region$ = "North" end add add cluster orders orders->customer$ = "Beta LLC" orders->amount = 1200 orders->region$ = "South" end add add cluster orders orders->customer$ = "Acme LLC" orders->amount = 5500 orders->region$ = "East" end add add cluster orders orders->customer$ = "Alpha LLC" orders->amount = 2200 orders->region$ = "West" end add

Collecting and iterating

collect cluster orders include orders->amount > 1000 sort by orders->customer$ end collect for each orders print orders->customer$; tab(25); orders->amount next orders

Descending Sorting

collect cluster orders include orders->amount > 1000 sort descending by orders->amount // incorrect syntax is sort by orders->amount descending end collect for each orders print orders->customer$; tab(25); orders->amount next orders
Problem: AI models generate loop variables like for order in orders and then access fields as order->amount. There is no such iteration variable in Sheerpower.

Solution: Inside a for each loop, use the cluster name itself with -> to access fields: orders->amount.

Efficiency: for each iterates the collected result set directly with no temporary object allocation.

Takeaway: The cluster name is both the collection and the current-row accessor. There is no separate iterator variable.

Loading from a CSV file

cluster sales: date$, product$, amount, region$ cluster input name '@sales_2025.csv', headers 1: sales

The file type picks the reader: .csv, .tsv, a .json array, or .jsonl -- JSON Lines, one object per line, the form logs and data exports use. cluster output name '@sales_out.jsonl': sales writes a cluster back out in any of those forms (the name chooses), one row per line for JSON Lines.

Always collect Before for each

Before iterating a cluster array with for each, collect the cluster. This step prepares the current result set for iteration. Without a collect cluster, a for each loop will generate an exception.

Problem: AI models trained on other languages expect collection iteration to work immediately. They generate for each loops without first collecting the cluster.

Solution: Use collect cluster before every for each iteration, even when no filtering or sorting is needed.

Efficiency: collect defines the active result set. It can also apply filtering, sorting, and selection before iteration begins.

Takeaway: In Sheerpower, collect is the required setup step before for each.

Wrong

for each orders print orders->customer$; " "; orders->amount next orders

Correct

collect cluster orders end collect for each orders print orders->customer$; " "; orders->amount next orders

Filtering and sorting before iteration

collect cluster orders include orders->amount > 1000 sort descending by orders->amount end collect for each orders print orders->customer$; " "; orders->amount next orders

Use findrow() for Single-Row Lookup

When looking up a single row in a cluster, use findrow(). Do not generate a full collect / include / for each / exit for loop just to find one matching row.

Problem: AI models often search for one row by generating a loop over the whole cluster. This is verbose and can be inefficient.

Solution: Use findrow() for direct lookup when the goal is to find one matching row.

Efficiency: findrow() is the standard Sheerpower way to perform fast single-row lookup, rather than scanning rows manually.

Takeaway: If the intent is "find the row," reach for findrow() before writing a loop.

Wrong pattern

found? = false collect cluster foods include foods->id$ = wanted_id$ end collect for each foods found? = true exit for next foods

Correct pattern

row = findrow(foods->id$, wanted_id$) if row = 0 then print "Food not found: "; wanted_id$ else print foods->name$ end if

The VIEW Statement

VIEW creates a zero-copy window into a source string. It does not allocate a new string — it references a position and length within the original buffer. The clauses must appear in the correct order.

Problem: AI models either omit VIEW entirely and generate slow substring loops, or get the clause ordering wrong.

Solution: Use the clause order shown below. INTO names the source. The remaining clauses such as MID, PIECE, MATCH, and related options describe how to locate the window.

Efficiency: No string copy occurs. For large files parsed line-by-line, this is orders of magnitude faster than building substrings in a loop.

Takeaway: When parsing delimited text, reach for VIEW before reaching for mid$() or string concatenation.

Extracting a segment using MID

data$ = "hello" VIEW v$ INTO data$, MID 2, 3 // character positions 2, 3, and 4 // v$ hasn't calculated positions yet PRINT v$ // NOW it evaluates to "ell" and caches it PRINT v$ // Reuses cached positions -- no recalculation! data$ = "world" PRINT v$ // Re-evaluates to "orl" because source changed

Extracting a delimited piece

csv_line$ = "Alice,Santos,42,Manila" VIEW first_name$ INTO csv_line$, PIECE ",", MATCH 1 VIEW last_name$ INTO csv_line$, PIECE ",", MATCH 2 VIEW age$ INTO csv_line$, PIECE ",", MATCH 3 print first_name$; " "; last_name$; " age "; val(age$)

Iterating all pieces in a string

data$ = "red,green,blue,yellow" for idx = 1 VIEW token$ INTO data$, PIECE ",", MATCH idx _ = len(token$) // using the view resolves it and sets _integer if _integer = -1 then exit for // -1: no such piece -- an empty one still counts print idx; ": "; token$ next idx

An empty piece is still a piece: "red,,blue" has three, and a blank line in a file is an empty one. So a loop ends on _integer, not on an empty view: use the view (len() forces it to resolve), and _integer is -1 once MATCH runs past the last piece. Read it in the very next statement, before anything else sets it.

Parsing a file line by line

sep$ = chr$(13) + chr$(10) log$ = fileinfo$("@app.log", "contents") for idx = 1 VIEW line$ INTO log$, PIECE sep$, MATCH idx _ = len(line$) // resolve the view: _integer = -1 past the last line if _integer = -1 then exit for // a blank line is an empty piece, not the end if contains(line$, "error") then print line$ next idx

Keywords That Do Not Exist in Sheerpower

The following keywords are common in other languages. None of them should be used in new Sheerpower code.

// These keywords do not exist in Sheerpower -- do not generate them: call // routines are invoked by name only return // compiles, but do not generate it: exit routine leaves a routine, the "returning" clause hands back a value throw // use: cause exception raise // use: cause exception catch // use: when exception in ... use ... end when except // use: when exception in ... use ... end when continue // not a loop keyword -- it exists only inside a USE block, to resume after the failing statement; in loops use: repeat do / iterate for new // no heap allocation; clusters and variables are declared, not constructed this // no object model; use cluster field access: clustername->field null // use '' for empty string, 0 for empty numeric void // routines with no returning clause simply have no return value function // use: routine ... with ... returning ... def // use: routine fn // use: routine break // use: exit do or exit for end for // for loops close with "next varname", not "end for" endif // must be written as two words: end if elif // one word: elseif (also "else if" is wrong) == // one = tests equality; there is no == x++ as a value // x++ is a STATEMENT only: y = x++ and arr(i)++ do not compile

Print and Output

Output uses print. Items are separated by semicolons. A trailing semicolon suppresses the newline. Tab alignment uses tab(n).

print "Hello, "; name$ // Simple output print "Total: "; total; " items" // mixed types -- no conversion needed print "Col1"; tab(20); "Col2"; tab(40); "Col3" // tabbed columns print // blank line print "No newline here"; // trailing semicolon suppresses newline print " -- continued on same line"

A number in a print list is padded: a space before it (the sign's place) and one after, so print "x = "; 42 shows x = 42 . A fraction below 1 prints with its leading digit, 0.25, as every other language prints it; str$() gives the same text without the padding. When a test compares output, run the program and paste what it printed rather than predicting the spacing.

Formatted numeric output

print sprintf$("%m", 1234567.89) // output: 1,234,567.89 print sprintf$("%.2f", price) // output: 1234567.89 print sprintf$("$%.2m", amount) // output: $1,234,567.89 print f$("Total: [[sprintf$('%.2m',]] for [[sprintf$('%]]") // f$ places the values; sprintf$ inside a slot formats them

sprintf$() Format Specifiers

Sheerpower uses sprintf$() for formatted string output. Do not assume that Sheerpower format strings are the same as C printf, JavaScript template strings, or Python format strings.

Several Sheerpower format specifiers are especially important because they appear often in real programs and have no direct equivalent in mainstream languages.

Problem: AI models often treat sprintf$() as if it were C printf or Python formatting. This leads to wrong assumptions about substitution, pluralization, money formatting, and number-to-words output.

Solution: Use the Sheerpower format specifiers shown below. Do not invent C-style or Python-style alternatives.

Efficiency: Built-in formatting keeps common output logic compact and avoids hand-written pluralization, comma insertion, and check-writing routines.

Takeaway: When generating formatted output in Sheerpower, first check whether sprintf$() already has the needed format specifier.

Common Sheerpower Format Specifiers

// Bare % -- general substitution for any type print sprintf$("Name: %, Score: %", name$, score) // %p -- automatic pluralization print sprintf$("% %p", item_count, "item") // %m -- comma-separated numeric output print sprintf$("%m", 1234567.89) // %w -- number-to-words output amount = 1234.56 print sprintf$("%w", amount)

The %p format is used for automatic pluralization. It takes the count and the singular word:

print sprintf$("% %p", 1, "item") // 1 item print sprintf$("% %p", 3, "item") // 3 items

The bare % format is a general substitution marker. It is not limited to one data type.

The %m format is commonly used for comma-separated numeric output, especially money-style output.

The %w format outputs the written-in-words version of a number, useful for check-writing and business documents.

Input from the User

input "Enter your name": name$ // prompts, reads string or number line input "Enter a sentence": sentence$ // reads entire line including spaces

Use line input when the input may contain spaces or commas. Use input for simple values.

Additional Built-In Patterns AIs Often Miss

Several small built-in routines and statements are common in real Sheerpower programs. AI models often invent mainstream-language substitutes for them.

Delimited string parsing

For delimited string work use split(text$ [, delim$]) to get every piece as a string array and join$(array [, delim$]) to write one back; element$() / elements() read one piece at a time. split() returns an ARRAY: its target must be dim words$(*), never a plain string.

csv$ = "red,green,blue" count = elements(csv$, ",") first$ = element$(csv$, 1, ",") second$ = element$(csv$, 2, ",")

Cluster row count

Use size() to get the number of rows in a cluster.

total_orders = size(orders)

String conversion

Use str$() to convert a number to a string. In Sheerpower, str$() does not add leading spaces.

msg$ = "Total: " + str$(total)

Operating system commands

Use pass to run an operating system command.

pass "dir > listing.txt" print "Exit status: "; _integer

The side channels: _integer, _real, _string$

Many built-ins leave a second result beside the one they return: _integer is a count or a position (where val() stopped, the row a findrow() found, the HTTP status of a URL open, a DLL's return), _real a measure (a p-value), and _string$ a text (the detail of the last exception, a server's error body). They are last-result values: read them in the next statement, not later and not inside a longer expression -- n = val(text$) then if _integer = 0 then is the numeric-test idiom.

Routine Scoping Rules

Sheerpower has four routine scopes. Generating the wrong scope is a common AI error, especially confusing local and private.

routine name // global -- all global variables and its own are accessible unless specified as private private routine name // private -- variables inside this routine are accessible only from within this routine scoped routine name // scoped -- variables scoped like private, but are cleared on entry and exit local routine name // local -- shares the parent routine's variable scope entirely

A routine with exactly one returning parameter can also be called as a function in any expression: calc_tax(amount = 1500, tax_rate = 0.12). Routines can return up to 16 values via the statement form. Sheerpower also has many dozens of built-in functions.

Problem: AI models use private when they mean local, or generate a with clause on a local routine that shares the parent's scope.

Solution: A local routine sees all of the parent routine's variables directly. It does not need parameters for data the parent already has. A private routine is isolated and must receive data through parameters.

Efficiency: Local routines avoid parameter overhead when decomposing a large routine into named steps.

Takeaway: If the sub-routine needs to read or write the parent's variables directly, use local. If it is a standalone reusable unit, use private or routine.
private routine process_invoice with inv total = 0 local validate_line_items local apply_discounts local format_output end routine local routine validate_line_items // sees inv, total directly from process_invoice's scope collect cluster inv if inv->qty < 0 then inv->qty = 0 end collect end routine local routine apply_discounts if total > 10_000 then total = total * 0.95 end routine local routine format_output print "Invoice total: "; sprintf$("$%.2m", total) end routine

Unique IDs

Whenever you need a unique ID, use the _GID$ special variable. It returns a 30-character, browser-safe identifier that combines a date stamp and a unique ID. The result is both globally unique and sortable by creation date.

Because _gid$ includes a universally unique identifier, two instances of the same handler running simultaneously will never produce the same value, even when running on different systems. This eliminates the need for counters, sequence generators, or inter-process coordination.

add table todos todos(id) = _gid$ // globally unique, generated locally todos(status) = 'open' todos(priority) = priority$ todos(description) = desc$ todos(last_datetime) = fulltime$ end add

Exception Handlers

Only wrap code in a when exception in block when you intend to handle the error differently than Sheerpower would on its own. Sheerpower already stops on unhandled exceptions and automatically reports the error type, description, call stack, and source location.

A handler earns its place only when it does something Sheerpower's automatic crash report cannot — such as retrying with a fallback, logging before continuing, or allowing the program to recover and keep running.

No handler needed:
cluster input name '@fruits.csv', headers 1: fruits
Handler earns its place:
when exception in cluster input name '@fruits.csv', headers 1: fruits use if extype = exceptiontype('filenotfound') then cluster input name '@fruits_default.csv', headers 1: fruits else stop end if end when
Takeaway: A use block that only prints extext$ and stops gives the user less information than Sheerpower's automatic crash report. Delete it and let Sheerpower handle it.
Critical rule: an exception raised INSIDE a use block is FATAL by design — it does not bubble to any other handler. Keep use blocks minimal: assignments, setting a flag, printing to an already-open channel. Do the risky recovery work AFTER end when, guarded by the flag:
failed? = false when exception in cluster input name '@fruits.csv', headers 1: fruits use failed? = true // nothing here can raise end when if failed? then cluster input name '@fruits_default.csv', headers 1: fruits end if

Routine Ordering — Major to Minor

Style guidance, not a compile rule — Sheerpower resolves forward references, so any order builds. Follow it anyway; it is the house convention.

Order routines so that every routine appears before the routines it calls. The main logic area comes first, then the high-level routines it calls, then the lower-level routines those call, and so on down to leaf routines at the bottom.

Think of it as a newspaper article — headline first, detail later. A reader should be able to start at the top and follow the logic downward without ever needing to jump back up.

// Correct order -- each routine appears before what it calls routine process_orders // high-level: called from main routine validate_order // mid-level: called by process_orders routine save_orders // mid-level: called by process_orders routine format_order_row // low-level: called by save_orders
Problem: Routines written in the order they were created force readers to jump back and forth to follow the logic.

Solution: Order routines top-down. High-level orchestrating routines first, helpers and infrastructure last.

Efficiency: In a large application this makes the file structure encode the call graph. You always know where to look, and you always understand the context of a routine before you reach its implementation.

Takeaway: Before writing a routine, confirm that the routines which call it have already appeared earlier in the file.

Writing Maintainable Sheerpower — the Professional Patterns

Style guidance, not compile rules — but generate these patterns by default in any program beyond a quick script.

Correct code and maintainable code are different goals. These patterns are how experienced Sheerpower developers structure programs that stay healthy for years.

1. Default to private routines and explicit globals

A plain routine shares the module's variable space: every bare variable inside it is global, and two routines using the same scratch name silently share it. Professionals default to private routine, pass data through parameters, and name true globals with a main$ prefix so every global access is visible at the call site:

main$tax_rate = 0.0825 // global: the prefix SAYS so private routine compute_total with amount, returning total total = amount * (1 + main$tax_rate) end routine

A maintainer reading main$tax_rate knows instantly it is shared state. A bare name tells them nothing.

2. Freeze configuration after loading it

Values that are loaded once and must never change afterward should be declared freezable and frozen after load. A later accidental write raises an exception instead of silently corrupting the configuration:

declare freezable main$api_url$ main$api_url$ = trim$(fileinfo$('@config_url.txt', 'contents')) freeze main$api_url$ // any later write raises an exception

3. Name every constant

A bare number in logic is a question every future reader must answer. Give it a name once:

const max_retries = 5 const label_width = 32 const senior_age = 65 if age >= senior_age then apply_discount

4. Give every routine a decorated header

The standard Sheerpower routine header documents the contract in four sections. Generate one for every routine; write none rather than omitting a section:

//****************************************************************** // c o m p u t e _ t o t a l //****************************************************************** // Brief description: // Applies the sales tax rate to an invoice amount. // // Expected on entry: // amount = the pre-tax invoice amount // main$tax_rate holds the loaded tax rate (e.g. 0.0825 = 8.25%) // // Locals used: none // // Results on exit: // total = amount with tax applied //******************************************************************

Documenting entry constants matters most: "what is 0.0825?" should never need asking twice. And if the routine has side effects — writes a file, changes a main$ global, adds cluster rows — say so under "Results on exit": side effects a maintainer cannot see from the call site are the ones that hurt.

5. Keep shared include files defensive

%include merges a file into the SAME variable space as the including program — it is not a namespace. In a .spinc file included by many programs, a bare local like count can collide with any future caller's constant of the same name. Either prefix the include's working variables with the include's own name, or wrap its routines in a module ... end module block, which gives real isolation.

6. Put growth in data, not code

When a program's content grows — more products, more rooms, more message templates — prefer a small generic engine reading CSV rows over adding a routine per case. New content then means new DATA ROWS with zero code change, and one generic validator checks all of it. Reserve per-case routines for genuinely one-off logic.

Make the Next Change Easier — Seven Habits

Style guidance, not compile rules — but generate these patterns by default in any program beyond a quick script.

The Professional Patterns above are about where data lives and how it is guarded. The seven habits below are about how the code reads and how it changes. They share one idea: make the next change easier. Software is rarely hard because of the first version written — it becomes hard when the next fifty changes are harder than they need to be.

1. Keep the main path easy to follow

Problem: A routine that must check several preconditions is easy to write as nested if blocks, one inside the other. The code works, but the actual operation ends up buried three or four levels deep, and every reader has to hold all the conditions in their head to find it.
Solution: Check each precondition up front and exit routine the moment one fails. The main path then reads top to bottom at the shallowest indentation.
Efficiency: Early exits cost nothing, and a reader finds the real work in the first screen of the routine.
Takeaway: Nesting is allowed; burying the important logic inside nesting is not. Guards first, work last.

Wrong (do not generate)

private routine ship_order with orders, order_id$, returning shipped? shipped? = false row = findrow(orders->id$, order_id$) if row <> 0 then if orders->status$ = "paid" then if orders->qty > 0 then // the actual work is buried three levels deep print f$("Shipping [[orders->qty]] of [[orders->sku$]]") shipped? = true end if end if end if end routine

Correct

private routine ship_order with orders, order_id$, returning shipped? shipped? = false // guards first -- each one exits immediately row = findrow(orders->id$, order_id$) if row = 0 then exit routine if orders->status$ <> "paid" then exit routine if orders->qty <= 0 then exit routine // the main path -- now easy to see print f$("Shipping [[orders->qty]] of [[orders->sku$]]") shipped? = true end routine

2. Name things by their meaning

Problem: Names like data, result, item, temp, and x force the reader to look somewhere else to learn what a value represents. Every one of them is a small piece of detective work.
Solution: Name a variable after what it holds (pending_order) and a routine after what it does (process_order). The name should say what the business concept is.
Efficiency: Good names reduce the time spent reading a codebase more than any other single habit, and they make Sheerpower's implied parameter passing (calc_tax with income, rate) read like prose.
Takeaway: Names need not be long. Make the concepts that carry weight obvious.

Wrong (do not generate)

routine process with data, returning result result = data * 1.12 end routine

Correct

const tax_multiplier = 1.12 routine total_pending_order with pending_amount, returning order_total order_total = pending_amount * tax_multiplier end routine

3. Keep external systems behind a boundary

Problem: Programs talk to systems they do not control — payment providers, vendor data feeds, email services, other APIs. When the external service's field names are used directly throughout the program, a rename on their side ripples through every routine that touches those fields.
Solution: Convert external data into the program's own cluster, with the program's own field names, in one routine. Everything downstream reads the program's cluster and never sees the vendor's names.
Efficiency: When the external service changes, one conversion routine changes. Nothing else does.
Takeaway: Keep external complexity at the edge. The rest of the program should not know the vendor exists.

Wrong (do not generate)

// vendor field names used everywhere in the program cluster vendor_feed: cust_nm$, amt_due, rgn$ cluster input name '@vendor_feed.csv', headers 1: vendor_feed collect cluster vendor_feed end collect for each vendor_feed print vendor_feed->cust_nm$; tab(30); vendor_feed->amt_due // vendor's names leak out next vendor_feed

Correct

// the vendor's shape -- only import_vendor_feed knows about it cluster vendor_feed: cust_nm$, amt_due, rgn$ // our shape -- everything else in the program uses this cluster customers: name$, balance_due, region$ routine import_vendor_feed cluster input name '@vendor_feed.csv', headers 1: vendor_feed collect cluster vendor_feed end collect for each vendor_feed add cluster customers customers->name$ = vendor_feed->cust_nm$ customers->balance_due = vendor_feed->amt_due customers->region$ = vendor_feed->rgn$ end add next vendor_feed end routine // downstream code never mentions cust_nm$ or amt_due collect cluster customers sort by customers->name$ end collect for each customers print customers->name$; tab(30); customers->balance_due next customers

4. Make invalid states hard to represent

Problem: When every field is "maybe present," every routine has to keep asking: does this exist, is it valid, can I do this? A single orders cluster where payment_id$ is sometimes "" means every shipping, invoicing, and reporting routine must re-check that the order was paid.
Solution: Represent state accurately. A row that reaches the paid_orders cluster has a payment id by construction, so nothing downstream needs to check. Use the ? suffix so a flag is a real boolean, name constants with const, and freeze configuration after loading so a later write raises an exception instead of silently corrupting it.
Efficiency: Fewer existence checks, fewer defensive branches, and a whole class of "how did this row get here" bugs that cannot occur.
Takeaway: You will not eliminate every error, but you can make the wrong state impossible to build in the first place.

Wrong (do not generate)

// one cluster, everything optional -- every consumer must re-check cluster orders: id$, customer$, amount, paid?, payment_id$ routine invoice_order with orders, order_id$ row = findrow(orders->id$, order_id$) if row = 0 then exit routine if orders->paid? = false then exit routine // asked here ... if orders->payment_id$ = "" then exit routine // ... and here ... print f$("Invoice for [[orders->customer$]], payment [[orders->payment_id$]]") end routine // ... and again in ship_order, report_paid, refund_order, etc.

Correct

// a row exists in paid_orders ONLY once payment has completed cluster orders: id$, customer$, amount cluster paid_orders: id$, customer$, amount, payment_id$, paid_on$ routine record_payment with orders, order_id$, payment_id$ row = findrow(orders->id$, order_id$) if row = 0 then exit routine if payment_id$ = "" then exit routine // the one place this is checked add cluster paid_orders paid_orders->id$ = orders->id$ paid_orders->customer$ = orders->customer$ paid_orders->amount = orders->amount paid_orders->payment_id$ = payment_id$ paid_orders->paid_on$ = date$ end add end routine // downstream: the state is guaranteed, so no re-checking routine invoice_order with paid_orders, order_id$ row = findrow(paid_orders->id$, order_id$) if row = 0 then exit routine print f$("Invoice for [[paid_orders->customer$]], payment [[paid_orders->payment_id$]]") end routine

5. Separate decisions from actions

Problem: A business rule ("only verified adults may use this feature") gets written in the same routine that updates the database and sends the email. The rule cannot be tested without triggering the side effects, so it rarely gets tested at all.
Solution: Put the decision in a routine that takes inputs and returns a verdict — no file I/O, no email, no database. Put the actions in a separate routine that acts on the verdict.
Efficiency: A decision routine is trivially testable with a self-judging test program (see Debugging and Test Methods That Work). This pattern applies to permissions, pricing, validation, retry policy, and notification rules.
Takeaway: Make important decisions easy to test without firing the side effects they control.

Wrong (do not generate)

routine grant_feature with user_id$, age, verified? if age >= 18 and verified? then // the rule is tangled with the actions open file log_ch: name "@feature.log", access append print #log_ch: f$("[[fulltime$]] granted [[user_id$]]") close #log_ch send_welcome_email with user_id$ end if end routine

Correct

const adult_age = 18 // DECISION -- pure, no side effects, easy to test private routine eligible_for_feature with age, verified?, returning eligible? eligible? = false if age < adult_age then exit routine if verified? then eligible? = true end routine // ACTIONS -- only run when the decision says so routine grant_feature with user_id$, age, verified? eligible_for_feature with age, verified?, returning eligible? if eligible? = false then exit routine open file log_ch: name "@feature.log", access append print #log_ch: f$("[[fulltime$]] granted [[user_id$]]") close #log_ch send_welcome_email with user_id$ end routine
// a self-judging test of the decision alone -- no log file, no email program test_eligible_for_feature option abort fails = 0 eligible_for_feature with age 17, verified? true, returning eligible? if eligible? then fails++ print "FAIL: 17 should not be eligible" end if eligible_for_feature with age 18, verified? false, returning eligible? if eligible? then fails++ print "FAIL: unverified should not be eligible" end if eligible_for_feature with age 18, verified? true, returning eligible? if eligible? = false then fails++ print "FAIL: verified adult should be eligible" end if print "fails = "; fails if fails > 0 then abort 33 abort 0 end

6. Make errors useful

Problem: An error that says "something went wrong" tells a developer nothing and gives the calling program nothing it can act on.
Solution: Every failure carries two things: a human-readable message and a stable numeric code. Text is for humans; codes are for systems. Name the codes with const, return them alongside the message, and when raising deliberately, use cause exception with the named code. When logging, include the context a developer needs — identifiers, the operation, the relevant state — and never log passwords, tokens, keys, or other secrets.
Efficiency: A caller tests one number instead of parsing a string, and a log line that names the order id saves a debugging session.
Takeaway: A message plus a code, with safe useful context. A bare "failed" is not an error report.

Wrong (do not generate)

routine check_shippable with orders, order_id$, returning ok? ok? = false row = findrow(orders->id$, order_id$) if row = 0 then print "Something went wrong" // which order? why? exit routine end if ok? = true end routine

Correct

// codes for systems const err_none = 0 const err_not_found = 1001 const err_not_paid = 1002 const err_out_of_stock = 1003 routine check_shippable with orders, order_id$, returning error_code, error_text$ error_code = err_none error_text$ = "" row = findrow(orders->id$, order_id$) if row = 0 then error_code = err_not_found error_text$ = f$("Order [[order_id$]] was not found") exit routine end if if orders->status$ <> "paid" then error_code = err_not_paid error_text$ = f$("Order [[order_id$]] has status [[orders->status$]], expected paid") exit routine end if if orders->qty <= 0 then error_code = err_out_of_stock error_text$ = f$("Order [[order_id$]]: [[orders->sku$]] has quantity [[orders->qty]]") exit routine end if end routine // the caller acts on the code and logs the text with context check_shippable with orders, order_id$, returning error_code, error_text$ select case error_code case err_none ship_order with orders, order_id$, returning shipped? case err_not_paid request_payment with order_id$ case else open file log_ch: name "@shipping.log", access append print #log_ch: f$("[[fulltime$]] code [[error_code]]: [[error_text$]]") close #log_ch end select

To stop the program deliberately with a code the crash report will carry, use cause exception with the named constant:

if quantity < 1 then cause exception err_out_of_stock

7. Keep each change focused

Problem: One edit that adds a feature, refactors an old routine, changes a cluster layout, and rewrites the retry logic may work perfectly — and be impossible to review, test, or roll back.
Solution: One change, one purpose. When asked to do several things, deliver them as distinct, clearly labeled edits, each compiled and tested on its own. Note anything you noticed but deliberately left alone with a %TODO so it is not lost and not mixed in.
Efficiency: This is the same discipline as one edit, one compile, one test. Sheerpower's compile speed makes it free; a focused change that fails points at exactly one cause.
Takeaway: Each change should be reviewable, testable, and reversible by itself. If a description of the change needs the word "and," it is probably two changes.

Wrong

// one edit that does everything at once: // - adds checkout validation // - renames three routines // - changes the orders cluster layout // - rewrites the retry loop // ... one compile, one failure, four suspects

Correct

// Change 1: add checkout validation -- compile, test, done // Change 2: rename routines for clarity -- compile, test, done // Change 3: extend the orders cluster -- compile, test, done // Change 4: rewrite the retry loop -- compile, test, done %TODO the discount routine also needs the boundary treatment (habit 3) -- separate change

The seven habits in one list

  1. Keep the important logic visible — guards first, work last, exit routine early.
  2. Use names that communicate intent — no data, result, item.
  3. Keep external dependencies contained — convert to your own cluster in one routine.
  4. Make invalid states hard to create — separate clusters per state, ? booleans, const, freeze.
  5. Separate decisions from side effects — pure decision routines, action routines that act on the verdict.
  6. Make failures understandable — a code for systems, text for humans, context in logs, never secrets.
  7. Keep changes focused — one edit, one purpose, one compile, one test.

Track State — Do Not Re-Derive It

A design lesson that follows from the habits above, and the single most useful smell to recognize.

When a routine has to know “where am I” or “what is the current value” — a position in a string, which field is being filled, whether a session is open — there are two ways to answer it. You can track it: keep a variable that holds the answer and update it as it changes. Or you can re-derive it: recompute the answer from the data every time, and guard the awkward cases. The first is a fact you maintain; the second is a guess you keep correcting.

The smell: you are adding a third or fourth guard to one routine — “but what if the delimiter was deleted”, “but what if the cursor moved”, “but what if the value spans two fields”. Each guard patches one case the re-derivation gets wrong, and the guards start interacting. That pile of special cases is the signal that the routine is re-deriving state it should simply be tracking.

The fix: hold the state in a variable and reduce the question to one check. A whole class of edge cases then cannot occur, because the answer is maintained, not re-guessed.

Takeaway: stacked guards are a design smell. One tracked value and one clear invariant beats five heuristics — and it is far easier to change later, because the next reader has one fact to understand, not five interacting exceptions.

Wrong (do not generate)

Re-finding the current field on every character by scanning for the delimiter that follows it — then guarding the cases where the scan goes wrong:

// find "the current field value" by re-deriving it each call routine current_value$ with line$, cursor, returning value$ value$ = "" // where does the field end? scan for the next separator ... stop_at = pos(line$[cursor:len(line$)], "|") if stop_at = 0 then stop_at = len(line$) + 1 // guard: no separator left if cursor > stop_at then exit routine // guard: cursor moved past it if contains(line$[cursor:stop_at], "{") then exit routine // guard: spans a hole // ... and another guard next week value$ = line$[cursor:stop_at - 1] end routine

Correct

Track the field's start and length; update the length by one as each character is added or removed. “Is the cursor still in this field?” becomes a single comparison, and the guards disappear:

// field_start and field_len are tracked; the caller reports each edit routine field_edited with line$, cursor, delta, returning value$ value$ = "" // the ONE invariant: the edit must be at the end of the tracked field if cursor - delta <> field_start + field_len then exit routine // not in the field -- done, no guessing end if field_len = field_len + delta if field_len < 0 then field_len = 0 value$ = line$[field_start:field_start + field_len - 1] end routine

Same idea everywhere: a parser that keeps its position instead of re-scanning from the top; a loop that carries a running total instead of re-summing; a handler that sets a ? flag when a session opens instead of inferring it from the data each time. If a routine keeps asking the data a question it could have remembered the answer to, remember the answer.

Calling a Web Service

A URL is a file: open file ch: name url$ fetches it (10-second timeout by default), line input #ch reads the reply, and jsonutil$(text$, '/path/to/value') picks a value out of JSON. A POST is open file ch: name 'http-post://' + quote$(url$), data body$, headers h$ (header lines one per line; a Content-Type line replaces the form default; verb 'PUT' changes the method). The reply is readable: ask #ch: status code, ask #ch, header 'Content-Type': value ct$. A reply of 400 or more raises the catchable filenotfound exception with the server's body in _string$ and the status in _integer -- read them in the use block. For a service called many times, set session 'api': base 'https://api.example.com', headers 'Authorization: Bearer ' + key$ then open file ch: name '/v1/items', session 'api': the base, the headers, the cookies and the timeout ride along, and a routine named in the session's call refreshes a credential on a 401.

when exception in open file ch: name 'https://api.example.com/v1/items/42' reply$ = '' do line input #ch, eof done?: line$ if done? then exit do reply$ = reply$ + line$ loop close #ch print 'name: '; jsonutil$(reply$, '/name') use print 'the service answered '; _integer; ': '; _string$ end when

CGI Web Handler Pattern

Sheerpower web applications commonly use a CGI-style handler pattern. This is not like Flask, Express, PHP, or browser-side JavaScript. Do not invent a web framework pattern from another language.

The common Sheerpower pattern is:

  • Open the CGI channel with open file cgi://.
  • Read request input with line input #cgi_ch.
  • Use getsymbol$() to read submitted values.
  • Use [[%spscript]] templates for generated output.
Problem: AI models have little or no training data for Sheerpower web handlers. They may invent routes, decorators, callbacks, request objects, JSON frameworks, or server APIs from other languages.

Solution: Use the Sheerpower CGI handler pattern shown in the relevant web tutorials and existing examples. Do not invent an Express-style, Flask-style, or PHP-style structure.

Efficiency: The CGI channel and template pattern keep request handling simple, direct, and easy to validate with the compiler.

Takeaway: For Sheerpower web code, follow the cgi://, line input, getsymbol$(), and [[%spscript]] pattern.

Pattern reminder

// CGI web handler pattern -- confirm details against the web tutorial open file cgi_ch: name "cgi://HANDLERNAME" line input #cgi_ch: request$ action$ = getsymbol$("action") // Use [[%spscript]] templates for generated web output.

Before generating a full Sheerpower web app, re-read the relevant web tutorial or a working web application. This pattern is a major Sheerpower pattern and should not be guessed from other languages.

Sheerpower Compiler Lifecycle

To write correct and efficient Sheerpower programs, it is important to understand how the compiler processes source code. The lifecycle is simple, predictable, and designed for both speed and reliability.

Problem:
Many developers assume a traditional multi-phase compiler with separate analysis, optimization, and linking stages. This leads to incorrect mental models, especially when reasoning about forward references, performance, and error behavior.

Solution:
Sheerpower uses a streamlined compilation model with four key stages: directive processing, incremental super-p-code generation, back-patching, and virtual machine (SPVM) execution.

Efficiency:
Because code is emitted as it is parsed, compilation is extremely fast. The final back-patching step resolves forward references without requiring multiple full passes or complex linking stages.

note: SheerPower super-pcodes preserve programmer intent, giving the SPVM the information it needs to optimize whole operations instead of interpreting individual instructions.

Takeaway:
Think of Sheerpower as a single-pass compiler with a lightweight fix-up phase. This mental model explains both its speed and its predictable behavior.

Compilation Stages

1. Directive Processing

Before compilation begins, directives such as %include, %debug, and related settings are processed. This stage determines which source files are merged and which compile-time behaviors are enabled.

The result is a fully assembled source stream that the compiler will process as a single, continuous program.

2. Incremental Super-P-Code Generation

As the compiler parses each statement, it immediately generates intermediate instructions, called super-p-code. There is no delayed code generation phase and no separate intermediate representation tree.

Problem:
Traditional virtual machines execute many small, low-level instructions, which increases interpretation overhead and reduces performance.

Solution:
Sheerpower uses super-p-code, where each instruction represents a higher-level operation, for example a full expression evaluation or data movement, rather than a single primitive step.

Efficiency:
By combining multiple low-level operations into a single instruction, the VM executes fewer steps, reducing dispatch overhead and improving runtime speed.

Takeaway:
Super-p-code is a "super-instruction" model: fewer, richer instructions that execute more work per step, making the VM both fast and predictable.

This direct emission model is a key reason Sheerpower can compile very large codebases in extremely short timeframes.

3. Back-Patching

After the initial pass, the compiler performs a fix-up phase known as back-patching. During this step, unresolved addresses are filled in for:

  • Routine calls made before the routine definition
  • Forward-referenced labels
  • Loop back-edges and control flow targets

This allows developers to write code in a natural top-down style without needing to predeclare or reorder definitions.

4. VM Execution

The finalized super-p-code is executed by the Sheerpower Virtual Machine. The VM uses highly optimized super-instructions to deliver consistent and predictable performance across platforms.

Because execution is handled by the VM, behavior remains stable and portable, independent of the underlying operating system.

Pre-Flight Check for Generated Code

Before submitting generated Sheerpower code, run through this checklist mentally:

  • No call keyword anywhere?
  • Routine invocations use with and returning?
  • Every block has a matching end ___ terminator?
  • For loops close with next varname — not end for?
  • Boolean variables end with ??
  • String variables end with $?
  • Loop restart uses repeat do for do loops or iterate for for for loops — not continue?
  • Loop exit uses exit do or exit for — not break?
  • Early routine exit uses exit routine — not return?
  • Exception handler uses retry or continue — not return?
  • for each loops access fields as clustername->field$?
  • collect cluster appears before every for each?
  • Every select case includes case else?
  • No throw, raise, catch, new, return, break, or end for?
  • VIEW clauses use target INTO source, PIECE delimiter, MATCH index?
  • A VIEW loop over pieces ends when _integer is -1 right after the view is used — not on an empty piece?
  • Number parsing from text goes through VAL() first?
  • Delimited strings use split() into a dim words$(*) array (or element$() / elements() for one piece), and join$() to write them back?
  • Single-row lookups use findrow() — not a full cluster scan?
  • A loop that writes one value to every n-th element is a fill x(start step n) with v or a slice store x[start:*:n] = v?
  • Large tables of true/false flags are boolean arrays, dim name?(n) — not integer arrays or bit masks?
  • Cluster row counts use size()?
  • Number-to-string conversion uses str$()?
  • Operating system commands go through pass?
  • Formatted output uses Sheerpower sprintf$() formats such as %, %p, %m, and %w?
  • CGI web handlers follow the cgi://, line input, getsymbol$(), and [[%spscript]] pattern?
  • Cluster routine parameters use root name only — no colon syntax?
  • No exception handler whose only action is print + stop?
  • Routines ordered major to minor — callers before the routines they call?
  • Preconditions checked up front with exit routine — main path not buried in nested if blocks?
  • No generic names (data, result, item, temp) where the business meaning is known?
  • External data converted to the program's own cluster and field names in one routine?
  • Business decisions in pure routines, separate from file, database, and email actions?
  • Every returned failure carries a named numeric code and a message with context — and no secrets in any log line?
  • This edit does one thing, and anything else noticed is left as a %TODO?
  • String literals contain no escape sequences — "\n" is "\" followed by "n"?
  • No exit do or exit for inside a use block — set a flag and exit after end when?
  • use blocks kept minimal so nothing inside them can raise a second exception, with follow-up logic testing _error after end when?
  • No var--, var+=, or var-= expressions?
  • var++ appears only as a statement, never inside an expression?

Vibe Coding Workflow: One Edit, One Compile, One Test

Vibe coding works best when the feedback loop is short. In Sheerpower, there is no reason to make a large batch of changes before checking the result. The compiler is fast enough that every meaningful code change should be followed immediately by validation.

Problem:
AI-generated code can contain small mistakes: a misspelled variable, a missing terminator, an incomplete statement, or a routine name that does not match the source. If many changes are made before compiling, these small mistakes become harder to isolate.

Solution:
Use a tight cycle: make one change, compile, read the result, fix the first error, and compile again. Do not guess blindly. Always read the compiler message and the source line it points to.

Efficiency:
Sheerpower compiles very quickly, so frequent validation is not a burden. It is the normal workflow. One edit followed by one compile keeps the AI, the programmer, and the source code on the same page.

Takeaway:
For Sheerpower vibe coding, the rule is simple: one edit, one compile, one test. Keep the loop tight.

Compile After Every Code Change

After each code change, compile and validate immediately:

sp4gl "nutrition_lookup_web.spsrc" /validate | Out-File -Encoding utf8 "nutrition_lookup_build.txt"

Always use PowerShell for this workflow, not bash. Bash can alter or mangle the line-delimited compiler output. The sp4gl command is expected to be on the system path, so a full path is not needed.

Use a build-output filename that matches the program name. For example, if the source file is:

weather_web.spsrc

then the build output should be:

weather_build.txt

This keeps build results easy to find and avoids overwriting results from other programs.

Check the Build Result

After compiling, check the exit code. If the build failed, read the build output:

sp4gl "weather_web.spsrc" /validate | Out-File -Encoding utf8 "weather_build.txt" if ($LASTEXITCODE -eq 0) { Write-Output "Clean build" } else { Get-Content "weather_build.txt" }

The build exit codes are:

  • 0 — clean build
  • 1 — compile error

A clean build looks like this:

$info |Build of C:\path\to\weather_web.spsrc $info |Generated 1K pcodes from 289 lines (525000/sec). $info | **** Clean build ****

When the Compile Fails

Compiler error lines follow this format:

$error |C:\path\to\file.spsrc |42 |7 |Unknown variable: totl_foods

The fields are:

  • filepath
  • line number
  • column number
  • error message

When a compile fails, follow this process:

  1. Read the build output and identify the first error.
  2. Read the source file at the reported line number.
  3. Also read a few lines before and after the reported line.
  4. Understand the problem from the error message and the nearby code.
  5. Fix the source file.
  6. Recompile and check the exit code again.

If there are multiple errors, focus on the first one. Later errors are often caused by the first error.

Do not guess at fixes blindly. The error message plus the surrounding source code is usually enough to show what went wrong.

For AI-assisted coding, never invent a fix based only on the error message. First inspect the source line, nearby lines, and the related variable or routine names. Then make the smallest correction that directly explains the compiler error.

Common Compile Errors

Error message Typical cause
Unknown variable Typo in a variable name
Unexpected end of statement Missing closing quote, parenthesis, or keyword
Expected end if Mismatched block structure, such as an if without end if
Unknown routine Typo in a routine name, or routine defined after stop

Example: Full Cycle with a Small Program

The following example shows the complete cycle: write, compile, fail, read the error, fix the source, recompile, run, and verify.

Step 1 — write the program:

// Hello World Example open file out_ch: name "@hello_world_result.txt", access output message$ = "Hello, world!" print #out_ch: mesage$ close #out_ch

Step 2 — compile the program:

sp4gl "hello_world.spsrc" /validate | Out-File -Encoding utf8 "hello_world_build.txt"

The exit code is 1. The build output contains:

$info |Build of C:\path\to\hello_world.spsrc $error |C:\path\to\hello_world.spsrc |5 |7 |Unknown variable: mesage$ $info | **** 1 error ****

Step 3 — read the source at line 5:

2 open file out_ch: name "@hello_world_result.txt", access output 3 message$ = "Hello, world!" 4 5 print #out_ch: mesage$ 6 close #out_ch

The problem is that mesage$ is misspelled. The variable created on line 3 is named message$. The fix is to use the same variable name in the print statement:

print #out_ch: message$

Step 4 — recompile. The build now succeeds:

$info |Build of C:\path\to\hello_world.spsrc $info |Generated 1K pcodes from 5 lines (5000/sec). $info | **** Clean build ****

Step 5 — run and verify:

sp4gl "hello_world.spsrc"

Then read hello_world_result.txt:

Where the file lives matters:

Put the program in the project folder, not in a temporary folder. Sheerpower refuses to run a program from %TEMP% (a security guard against dropped scripts) and stops with Cannot run SheerPower from a TEMPORARY folder — under automation that looks like a hang, because the message is a dialog waiting to be dismissed.

Hello, world!

The output is correct. The cycle is complete.

Testing Web Programs

After a clean build, test web programs in a browser. The compiler verifies code correctness, but only the browser reveals user-interface issues such as wrong layout, missing data, broken buttons, or incorrect error messages.

Most web programs begin by making sure the SPINS web server is running:

pass 'sp4gl ' + fileinfo$('@spins_startup.spsrc')

To run a web program:

sp4gl "nutrition_lookup_web.spsrc"

The program will print a URL to its GUI console, typically:

Nutrition Lookup ready. 7731 foods loaded. go to http://localhost/nutrition_lookup.html

Open that URL in a browser and test the main workflow and edge cases:

  • Does the page load correctly?
  • Do buttons and inputs work?
  • Does the data display correctly?
  • Do error states show the right messages?

The Vibe Coding Cycle

The full vibe coding cycle is:

  1. Make a change to the .spsrc file.
  2. Compile with /validate.
  3. Check the exit code.
  4. If the build fails, read the error, read the source line, fix the source, and recompile.
  5. If the build succeeds, test the output.
  6. For console programs, run the program and read the output.
  7. For web programs, run the program and test it in the browser.
  8. Repeat for each change.

One edit, one compile, one test. Keep the loop tight.


Running Sheerpower Programs "Headless"

When an AI is helping to write or modify Sheerpower programs, it needs a simple and reliable development loop:

  1. Write or edit the program.
  2. Compile it.
  3. Provide input if needed
  4. Run it.
  5. Capture the output.
  6. Check the result.
  7. Fix any error.
  8. Repeat.

This loop works best when both compile errors and runtime errors are visible to the AI. The AI should be able to run the program, see what happened, and make the next correction without needing a human to press keys at the console.

Use option abort

Programs that are being tested by an AI should normally use:

option abort

With option abort, any uncaught runtime exception causes the program to exit with a failure status. This lets the AI detect that the run failed. When the run fails, Sheerpower also writes out an error file containing the state of the program when it failed.

Without option abort, Sheerpower may stop at the console prompt so a human developer can debug interactively. That is useful for a developer, but not useful for an unattended AI test run.

Interactive programs and /headless

Some console programs are interactive. They use line input to ask the user for information:

print "What is your name?" line input name$ print "Hello, "; name$ ; "!"

Run normally, this program waits for a person to type a name. That is correct for human use, but it prevents an AI from running the program in a fully automated way.

The /headless option solves this. Under /headless, line input reads from piped stdin instead of the keyboard. Each input line answers one line input, in order.

This means the same ordinary interactive program can be used in two ways:

  • A human can run it normally at the console.
  • An AI can run it automatically with /headless.

No special AI mode is needed. No duplicate test version is needed. The same source code is tested and used.

What /headless does

When a program is run with /headless:

  • line input reads from piped stdin instead of the keyboard.
  • Each line input consumes one input line.
  • At end of input, line input returns EOF as true instead of blocking.
  • print writes to capturable stdout.
  • delay does not pause; it becomes a no-op.
  • Numeric input and key input read from piped stdin too (one line per prompt; a key is the line's first character), and every prompt and answer is echoed to stdout as on a screen.
  • A menu prints its items numbered and reads the pick from the next line — see Testing a console application when nobody is at the keyboard below.
  • The program can tell: the system variable _headless is 1 under /headless and 0 in a window — a true-or-false value, like _debug and _test. A test that only makes sense in a window (one that types into a real screen with set window: typeahead) guards itself with if _headless then exit routine, so one test file serves both ways of running.
// Expect: headless 1 (run with /headless) // headless 0 (run in a window) print 'headless '; _headless if _headless then print 'answers come from the pipe' else print 'answers come from the keyboard'

End-of-input behavior is especially useful for prompts such as:

print "Press ENTER to exit" line input dummy$

In a normal console, the user presses ENTER. In a headless test, there may be no more input. In that case, dummy$ becomes "", and the program continues instead of blocking forever.

The AI test cycle using /Headless Option

The recommended AI vibe coding cycle using the /Headless Option is:

  1. Edit the .spsrc file.
  2. Compile with /validate.
  3. Check the compile exit code.
  4. Fix compile errors, if any.
  5. Create an input file, with one line for each line input.
  6. Run with /headless.
  7. Check the run exit code.
  8. Read the captured output.
  9. Compare the actual output with the expected output.
  10. Fix the program and repeat.

Once tests pass, periodically compile with option insights and review <program>_insights.json for complexity/unused-code drift. Run this at feature-completion, or when a routine keeps needing edits -- not every cycle.

The goal is simple:

One edit. One compile. One headless run. One verification.

Full example

Source file: greet.spsrc

program greet print "What is your name?" line input person$ print "Hello, "; person$; "!"

Compile:

sp4gl "greet.spsrc" /validate

Input file: greet_in.txt

Sally

Run headless:

cmd /c "sp4gl greet.spsrc /headless < greet_in.txt > greet_out.txt"

Expected output in greet_out.txt:

What is your name? Hello, Sally!

The program has now been tested automatically, even though it was written as a normal interactive console program.

Multiple prompts

If a program has more than one line input, the input file should contain one line for each prompt.

program add_two_numbers print "First number?" line input a$ print "Second number?" line input b$ total = val(a$) + val(b$) print "Total: "; total

Input file:

10 25

Run:

cmd /c "sp4gl add_two_numbers.spsrc /headless < add_in.txt > add_out.txt"

Expected output:

First number? Second number? Total: 35

The first input line is consumed by the first line input. The second input line is consumed by the second line input.

Use stdout for test results

When testing with /headless, use print for ordinary program output:

print "Result: "; result

The AI can read the captured stdout file and verify the result.

Avoid writing temporary result files from inside the program unless creating a file is the actual purpose of the program. For ordinary console tools, stdout is the cleanest test output.

Use cmd-level redirection

Use this form to run a headless test:

cmd /c "sp4gl prog.spsrc /headless < input.txt > output.txt"

The < redirects the input file into the program. The > captures stdout into the output file.

Quote paths that contain spaces:

cmd /c "sp4gl ""C:\my folder\prog.spsrc"" /headless < ""C:\my folder\input.txt"" > ""C:\my folder\output.txt"""

Do not rely on PowerShell pipeline capture for this workflow. Use cmd /c redirection, or use a real test harness that supplies stdin and captures stdout directly.

Instructions to give the AI

A good instruction for AI-assisted Sheerpower development is:

Edit the program. Compile with /validate. If there are errors, fix them. Create an input file for each line input. Run with /headless using cmd /c redirection. Check the exit code. Read the output file. Verify the result. Repeat until the program works.

Testing a console application when nobody is at the keyboard

A console application is more than line input: it asks with input, reads single keys, and offers menus. Under /headless every one of them takes its answer from the next line of the piped file, and every prompt and answer appears in the transcript exactly as a person would have seen them on the screen.

  • input and line input take one line each. The prompt is written, then the answer, then a line ending — so the transcript reads How many widgets? 12.
  • key input takes one line and uses its first character.
  • A menu prints its items numbered and reads the pick from the next line: an item number, or the start of an item's text (case does not matter). A submenu prints and reads again. An empty line is Escape; exit is Ctrl+Z (and sets _exit); a %multi menu takes a list such as 1,3.
  • When the answers run out, line input returns an empty string, numeric input raises "End of file on input", and a menu raises it too — a short answer file fails loudly instead of hanging.

A menu program and its transcript. The answers file holds two lines, 3 and papaya:

print 'Welcome to the fruit stand' spec$ = '%title ' + quote$('Pick a fruit') + ', ' + 'apples, pears, ' + '%bar, ' + 'tropical = { mango, papaya }' line input menu spec$: choice$ print 'you chose '; choice$ print 'path '; _string$
cmd /c "sp4gl fruit.spsrc /headless < answers.txt > run.txt"
Welcome to the fruit stand Pick a fruit 1. APPLES 2. PEARS ------ 3. TROPICAL > menu> 3 TROPICAL 1. MANGO 2. PAPAYA menu> papaya you chose PAPAYA path #3;#2

The numbered list is the menu's text form for the transcript; a person sees the menu itself. Everything the program printed is there to assert on, and the pick is echoed after menu>, so the transcript reads as a session.

Seeing the screen: /snapshots

A transcript shows what was printed, not where it landed. Add /snapshots and the screen is captured as a text file at every moment a person would be looking at it — each input wait, each menu waiting for its pick, and the end of the run:

cmd /c "sp4gl fruit.spsrc /headless /snapshots < answers.txt > run.txt"

Each capture is written beside the program as fruit_screen_1.txt, fruit_screen_2.txt, and so on: the screen as a grid of characters, then ---attributes--- and the same grid with one letter per cell (. plain, r reverse video, R G Y B M C W a colour, # a frame cell, i an input field), then ---info--- with the size, the cursor and the reason for the capture. A menu is drawn into the image exactly where and how a person would see it. The first capture of the fruit-stand run, trimmed to its top rows:

Welcome to the fruit stand +---------Pick a fruit---------+ | APPLES | | PEARS | |------------------------------| | TROPICAL > | +------------------------------+ ---attributes--- ................................................................................ ................................................................................ ................................................................................ ...##############################################............................... ...#rrrrrrrrrrrrrrrrrrrrrrrrrrrrrr#............................................. ---info--- rows=30 cols=80 cursor=2,1 at=menu

An AI can read these files directly and assert on them — that the frame is where %at put it, that the right item is highlighted (the reverse-video r cells), that a prompt sits at its at position, that the last print landed on the expected row — and a person can read them too. Combined with the transcript, a console application can be tested completely without anyone at the keyboard.

Summary

The /headless option makes interactive Sheerpower programs easy to test automatically.

It lets the AI feed input, capture output, avoid blocking, skip delays, detect runtime failures, and verify results.

The developer gets a normal interactive program. The AI gets a reliable automated test loop. That is the ideal setup for vibe coding.


Debugging and Test Methods That Work

These four methods come from extensive AI-driven Sheerpower development. Each one replaces a slower or less reliable habit.

1. Probe before asserting

When you are unsure how a function or statement behaves, do not reason about it, do not guess, and do not ask the user — test it. Write a minimal throwaway program, compile it (compiles take milliseconds), run it, and read the answer. This is faster and more reliable than any other option, and it should be your FIRST resort, not your last:

program probe_element_default // question: what is element$'s default delimiter? print "["; element$("alpha, beta, gamma", 2); "]" end

Sixty seconds of probe beats an hour of confident wrongness. If you then state the behavior to the user, you are quoting a measurement, not a memory.

2. Make test programs judge themselves

A test program should check its own results and report a verdict through its EXIT CODE — abort 0 for pass, a distinct nonzero code per failure kind. The caller then reads one number instead of parsing output:

program test_my_feature option abort fails = 0 if val("12" + ".") <> 12 then fails++ print "FAIL: trailing dot, got "; val("12" + ".") end if if len(trim$(" x ")) <> 1 then fails++ print "FAIL: trim$ length" end if print "fails = "; fails if fails > 0 then abort 33 abort 0 end

Print a detail line for each failure so the log explains itself, and one summary line on success. Never print a pass/fail verdict without also setting the matching exit code.

3. Beware constant folding in tests

Sheerpower evaluates LITERAL expressions at compile time. A test like val("abc") fails during the BUILD — so it tests the compiler, not the running engine, and a when exception block around it never runs. To test runtime behavior, build the value at runtime:

s$ = mid$("Xabc", 2, 3) // "abc", constructed at RUN time caught = 0 when exception in v = val(s$) // now the RUNTIME raise is exercised use caught = extype end when if caught = 0 then abort 34

Any exception or domain test with purely literal arguments is suspect. Route the value through mid$(), a variable, or a cluster field first.

4. Read the crash file — do not guess

With option abort, an uncaught exception writes <source>_error_<timestamp>.txt next to the program: the failing line, the call stack, and a full variable dump. After any unexpected exit-1, READ THAT FILE FIRST. It usually answers the question directly; diagnosing without it is speculation.

If a probe might crash HARD (no error file), write progress markers to a file and close it between steps, so the output survives:

open file ch: name "@probe_out.txt", access output print #ch: "before the risky call" close #ch // flushed -- survives a crash // ... the risky statement ... open file ch: name "@probe_out.txt", access append print #ch: "survived" close #ch

Where the output stops tells you where it died. And for a HUNG program, sp4gl debug <PID> from another terminal dumps the running program's state without killing it.

5. When a fix matters, pin it

After fixing a real bug, write a small self-judging test that FAILED before the fix and passes after — and keep it. A fix without a test is a fix that can silently unhappen. Verifying the test fails on the broken version is the important half: a test that never failed has proven nothing about its own ability to detect the bug.

(Show/Hide Key Takeaways)
Hide Description

    

       


      

Enter or modify the code below, and then click on RUN

Looking for the full power of Sheerpower?
Check out the Sheerpower website. Free to download. Free to use.