Skip to content

Repository files navigation

LBLBASIC

LBLBASIC is a small preprocessor for classic line-numbered BASIC.

It lets you write BASIC using symbolic labels and a few structured-control extensions, then converts that source into ordinary numbered BASIC suitable for interpreters such as Microsoft MBASIC under CP/M.

LBLBASIC source files conventionally use the .bal extension: BASIC with labels.

The project contains four main files:

  • lblbasic.py — the reference implementation used to develop and test language changes. It uses only the Python standard library.
  • lblbasic.awk — the original reference implementation. It is retained as a compact, portable implementation of the language as it stood when the reference was ported to Python.
  • lblbasic.bal — an implementation of LBLBASIC written in BAL itself.
  • lblbas.bas — the pre-converted MBASIC version of lblbasic.bal. The shorter filename is intentional for CP/M filesystems.

The Python and MBASIC versions are intended to implement the same BAL language. The test suite also checks the Python port against awk for the features they share.

The implementations intentionally have different feature boundaries:

Capability Python BAL/MBASIC Legacy awk
Labels and symbolic numeric substitution Yes Yes Yes
Structured IF, WHILE, DO, EXIT, and CONTINUE Yes Yes Yes
OFF/ON suppression and preserved blank lines Yes Yes Yes
Configurable source characters and line-number limits Yes Yes Yes
INCLUDE source files Yes Yes No
CONST numeric symbols Yes Yes No
OPT/ENDOPT, optional NOT, and ERROR Yes Yes No
Numeric STACK/PUSH/POP Yes Yes No
Trim, newline, and label-only comment CLI options Yes No No

Despite that difference, lblbasic.bal deliberately uses only the common legacy subset in its own executable source. The awk translator can therefore still generate the native MBASIC implementation.


Quick Start

Given a BAL source file:

':start
print "Hello"
goto @done

':unused
print "You should not see this"

':done
end

run:

python3 lblbasic.py program.bal > program.bas

The output will be ordinary numbered BASIC, for example:

10 print "Hello"
20 goto 40
30 print "You should not see this"
40 end

For CP/M/MBASIC, convert generated files to DOS/CP/M line endings before transferring them if necessary:

unix2dos program.bas

The same applies to .bal source files that will be read directly by the MBASIC implementation.


LBLBASIC in Action

The included sample/hilo.bal game demonstrates structured conditionals and loops, symbolic labels, compile-time options, and MBASIC random number generation. On the left is part of the readable BAL source; on the right is the translated program running under MBASIC on a CP/M system.

BAL source Running under MBASIC
A portion of the HILO game written in BAL, showing structured loops and conditionals The translated HILO game running under MBASIC on a green-screen CP/M system

BAL Language

BAL is intentionally a small extension of ordinary BASIC.

The preprocessor does not parse or evaluate BASIC expressions. Expressions remain BASIC text and are evaluated later by the target BASIC interpreter.

The main additions are:

  • symbolic labels
  • symbolic line-number references
  • structured IF
  • WHILE loops
  • DO loops
  • EXIT and CONTINUE
  • source suppression with OFF / ON
  • textual source inclusion
  • named numeric constants
  • optional compile-time source blocks
  • a numeric save/restore stack

Everything else is passed through as BASIC.


Symbolic Labels

A label definition is written as an apostrophe followed by the label-definition character, which defaults to ::

':start

A reference uses the symbolic-reference character, which defaults to @:

goto @start
gosub @worker

Labels are case-insensitive:

':Finished
goto @finished

is valid.

Leading and trailing whitespace in a label definition is ignored.

Labels may contain letters, digits, and underscores, but the first character must be a letter or underscore.

Examples:

':main
':read_record
':retry2
':_internal

Symbolic References Are Numeric Values

A symbolic reference is not limited to GOTO or GOSUB.

Outside quoted strings and comments, a symbolic reference is simply replaced by the BASIC line number assigned to that label.

For example:

goto @foo
gosub @foo
if x then @foo
on n goto @one,@two,@three
print 10 + @foo

are all legal BAL syntax.

The last form may not be especially useful, but BAL deliberately does not try to decide where a line number is semantically appropriate.

References inside quoted strings and comments are not replaced:

print "goto @foo"
' this mentions @foo but does not reference it

Undefined labels are left unchanged and produce a warning.


Label and Comment Output

BAL label definitions are also comment-like source lines:

':foo

Whether label lines and comments appear in generated output depends on the comments / COMMENTS setting.

With comments disabled, label-definition lines do not consume generated BASIC line numbers unless targetrem / TARGETREM requires the label line itself to be the branch target.

The Python translator can preserve label-definition comments independently with -L/--keep-label-comments. The symbolic label then refers to the following generated line rather than its own comment line.

Labels in the reserved __LBLB_ namespace are generated implementation details. Their definitions are never emitted and never become comment targets, regardless of comment or target-comment settings.

Blank and whitespace-only source lines are discarded by default. The Python translator's -k/--keep-blanks option preserves each one as a bare, unnumbered output line. Preserved blanks do not consume generated BASIC line numbers. Legacy awk supports the equivalent -v keepblanks=1 setting; BAL/MBASIC uses KEEPBLANKS.


Named Numeric Constants

Python and BAL/MBASIC support named signed-decimal integer constants:

'CONST! UMASK=125
'CONST! OFFSET=-20

X=X+@UMASK
Y=Y+@OFFSET

CONST! uses the configured structured suffix and emits no BASIC line. The name follows the same identifier rules as a symbolic label, while the value is restricted to an optional + or - followed by decimal digits. Expressions, fractions, exponent notation, type suffixes, and trailing comments are not accepted.

Constants and labels use separate internal tables but share the configured symbolic-reference character (@ by default). A reference may precede its constant definition. Names are case-insensitive, and a name may not be defined more than once or used as both a label and a constant. Constant directives in an OFF! region are ignored; definitions in included files share the parent translation's constant namespace. References are not expanded inside strings or comments.

The legacy awk implementation remains at its pre-constant feature set.


Optional Source Blocks

Python and BAL/MBASIC can include or suppress source according to a literal or an earlier constant definition:

'CONST! DEBUG=1

'OPT! @DEBUG
PRINT "debugging is enabled"
'ENDOPT!

'OPT! NOT @DEBUG
PRINT "included only when debugging is disabled"
'ENDOPT!

'OPT! 0
PRINT "this source is omitted"
'ENDOPT!

Zero suppresses the block; any nonzero signed decimal integer includes it. The operand must be either one integer literal or one constant reference, optionally preceded by case-insensitive NOT to invert the result. An OPT! constant must have been defined earlier because optional-source state is decided while the source is being read; ordinary constant substitutions may still use forward references.

OPT! blocks may nest. Within an already suppressed OPT! or OFF! region, nested option operands are not evaluated, although OPT! and ENDOPT! are still balanced. Directives and suppressed source emit no BASIC lines. Both keywords use the configured structured suffix. Legacy awk does not implement optional blocks.

An active optional configuration can deliberately stop translation with a source-defined diagnostic:

'OPT! NOT @IGUESS
'ERROR! You forgot to enable IGUESS
'ENDOPT!

ERROR! treats the trimmed remainder of its line as plain message text. It performs no symbolic substitutions, emits no BASIC line, and makes translation fail without partial output. An empty message is an error. Like other preprocessor directives, it is ignored inside suppressed OPT! and OFF! regions and uses the configured structured suffix.


Including Source Files

The Python reference supports textual source inclusion:

'INCLUDE! common.bal

The directive must occupy its physical source line apart from leading or trailing whitespace. Its filename is the trimmed remainder of the line; quotation marks and trailing comments have no special syntax. The directive is case-insensitive, uses the configured structured character, and is not emitted even when comments are preserved.

An included file behaves as though its physical lines replaced the directive. Labels, structured blocks, source-suppression state, and generated line numbering are shared across file boundaries. Includes inside an active OFF! region are ignored without attempting to open the file.

In Python, relative filenames are resolved from the directory containing the including file. An include read from standard input is initially relative to the current working directory. Under MBASIC, filenames are passed directly to CP/M, so an unqualified filename is relative to the active drive and user area.

Nested includes are limited to 20 levels, active recursive cycles are errors, and including the same file again after it has returned is allowed. The BAL/MBASIC implementation retains bounded memory by closing a parent file while reading its child, then reopening the parent and skipping back to its saved physical line. The legacy awk implementation remains at its pre-include feature set.


Numeric Save/Restore Stack

The Python and BAL/MBASIC implementations provide a small numeric stack for preserving scalar variables around subroutine calls:

STACK! 200

PUSH! X%,Y!,Z#
GOSUB @worker
POP! X%,Y!,Z#

STACK! accepts a positive integer literal from 1 through 32767. An apostrophe comment may follow the size after whitespace, as in STACK! 64 ' save up to 64 values. It must appear exactly once and before executable source, although labels and comments may precede it. PUSH! and POP! accept comma-separated scalar numeric variables whose names begin with a letter and contain letters or digits, with an optional %, !, or # suffix. Strings, arrays, expressions, empty operands, and the reserved LBLBSTACK and LBLBSP names are rejected. Whitespace around variables and commas is allowed. A trailing apostrophe comment may follow the complete variable list on PUSH! and POP!, just as it may on STACK!. An apostrophe comment without any preceding operands is still an empty-list error.

Values are restored positionally: after PUSH! X,Y,Z, POP! A,B,C assigns the saved values of X, Y, and Z to A, B, and C respectively. Stack state is global and crosses include and structured-block boundaries. If you examine the generated code, you'll see that POP! works "backwards" -- so Z is the first item on the stack, Y is the 2nd an X is the 3rd. Do not reverse the stack order manually. That is the correct way to restore PUSH! X,Y,Z is NOT POP! Z,Y,X but POP! X,Y,Z.

The generated array is LBLBSTACK# by default. -y/--stack-type selects int, single, or double storage, generating %, !, or # respectively. Conversion is left to BASIC; for example, choosing an integer stack knowingly allows fractional values to be rounded or rejected by the target.

Overflow and underflow checks are generated by default. -b or --no-stack-checks omits them; -B or --stack-checks explicitly enables them. PUSH and POP operations are emitted as colon-separated BASIC lines and are rejected if expansion would exceed 255 characters.

The BAL/MBASIC implementation provides equivalent configuration variables near the beginning of lblbasic.bal and lblbas.bas:

STACKTYPE=3   ' 1=integer, 2=single, 3=double
STACKCHECK=-1 ' nonzero enables overflow and underflow checks

These defaults select double storage with checks enabled. Set STACKCHECK=0 to omit the generated checks.


Structured IF

BAL provides a block form of IF.

if x=y then!
    print "equal"
endif!

An optional ELSE! is supported:

if x=y then!
    print "equal"
else!
    print "not equal"
endif!

The THEN! must be the final non-whitespace text on its physical source line.

ELSE! and ENDIF! must appear alone on their source lines apart from whitespace.

The implementation uses generated internal labels and branches rather than trying to negate the BASIC expression. Conceptually:

if x=y then!
    print "yes"
else!
    print "no"
endif!

is lowered to logic equivalent to:

IF x=y THEN <true-block>
GOTO <false-block>
<true-block>
PRINT "yes"
GOTO <end-block>
<false-block>
PRINT "no"
<end-block>

This keeps BAL from needing to understand BASIC operator precedence or expression semantics.

Structured IF blocks may be nested.


WHILE / WEND

BAL supports top-tested loops:

while! x<10
    print x
    x=x+1
wend!

The expression following WHILE! is passed unchanged to BASIC.

The loop continues while the expression is true.

WEND! must appear alone on its source line apart from whitespace.

Loops may contain nested IF, WHILE, or DO blocks.


DO / LOOP

An unconditional loop is:

do!
    print x
    x=x+1
loop!

BAL also supports bottom-tested forms.

LOOP WHILE

do!
    print x
    x=x+1
loop! while x<10

The loop repeats while the expression is true.

LOOP UNTIL

do!
    print x
    x=x+1
loop! until x>=10

The loop repeats until the expression is true.

As with the other structured constructs, the expression itself is not parsed by LBLBASIC.


EXIT

EXIT! exits the innermost enclosing WHILE! or DO! loop:

while! x<100
    x=x+1

    if x=50 then!
        exit!
    endif!
wend!

EXIT! may appear inside nested IF blocks. LBLBASIC searches outward for the innermost active loop.

Using EXIT! when there is no enclosing WHILE or DO block is an error.


CONTINUE

CONTINUE! skips to the next iteration of the innermost enclosing loop.

For a WHILE loop:

while! x<100
    x=x+1

    if x=50 then!
        continue!
    endif!

    print x
wend!

CONTINUE! returns to the WHILE condition test.

For a DO loop:

do!
    x=x+1

    if x=5 then!
        continue!
    endif!

    print x
loop! while x<10

CONTINUE! jumps to the loop-bottom test. This is important for LOOP! WHILE and LOOP! UNTIL: the bottom condition is still evaluated.

Using CONTINUE! outside a WHILE or DO block is an error.


Temporarily Disabling Source

BAL provides a preprocessing facility similar in spirit to:

#if 0
...
#endif

in C.

Use:

'OFF!

to stop emitting source, and:

'ON!

to resume.

Example:

print "before"

'OFF!
print "this code does not exist in the generated program"
goto @missing_label
':temporary
'ON!

print "after"

Everything between OFF! and ON! is ignored completely:

  • no BASIC output is generated
  • no BASIC line numbers are allocated
  • labels are not defined
  • symbolic references are not checked
  • structured blocks inside the disabled region do not affect the active block stack

OFF! / ON! blocks may be nested.

An unmatched ON! or end-of-file with an unterminated OFF! block is an error.


Nesting

Structured constructs use a block stack and may be nested.

For example:

while! x<20

    if x=5 then!
        x=x+1
        continue!
    endif!

    do!
        y=y+1

        if y=10 then!
            exit!
        endif!

    loop! while y<20

    x=x+1
wend!

Closing constructs must match the active block.

For example, a WEND! encountered while the top-level active block is an IF produces an error rather than silently generating incorrect code.


Configurable Syntax Characters

The three BAL extension punctuation characters are configurable.

The defaults are:

@   symbolic reference prefix
!   structured-keyword suffix
:   label-definition marker

Thus the normal syntax is:

':foo
goto @foo
if x then!
endif!

Python version

The Python implementation accepts long options:

python3 lblbasic.py \
    --labelchar='%' \
    --structchar='?' \
    --defchar=';' \
    program.bal

Other Python options include:

  • -c, --keep-comments, or --comments preserves comments; -C or --ignore-comments explicitly discards them. Comments are ignored by default.
  • -t or --target-comments preserves label-definition comments and makes labels refer to those comment lines. --targetrem remains as a compatible alias. -T or --no-target-comments explicitly selects the default.
  • -L, --keep-label-comments, or --keep-labels preserves label-definition comments without making them their own branch targets. Ordinary comments remain discarded unless -c is also used.
  • -n, --dos-newlines, or --cpm-newlines emits CRLF line endings ready for DOS or CP/M transfer. The default is Unix LF; --unix-newlines states that choice explicitly.
  • -s/--start and -i/--increment configure generated line numbering.
  • -l/--labelchar, -S/--structchar, and -d/--defchar configure BAL's three extension characters.
  • -m or --trim removes leading and trailing spaces and tabs from every physical source line. It does not alter whitespace inside strings or in the interior of a line.
  • -k or --keep-blanks preserves blank and whitespace-only physical lines as bare, unnumbered blank output lines. They do not consume generated BASIC line numbers. --trim also normalizes whitespace-only lines to bare blanks.

For example, to generate a CP/M-ready file while discarding ordinary source comments:

python3 lblbasic.py -C -n program.bal > program.bas

Legacy awk version

The awk implementation can override them with -v:

awk \
    -v labelchar='%' \
    -v structchar='?' \
    -v defchar=';' \
    -f lblbasic.awk program.bal

The equivalent source syntax would then be:

';foo
goto %foo
if x then?
endif?

MBASIC/BAL version

The corresponding variables near the beginning of lblbasic.bal / lblbas.bas are:

LC$="@"        ' symbolic-reference prefix
SC$="!"        ' structured-keyword suffix
DC$=":"        ' label-definition marker

Change these before running the program if different syntax is desired.

The BASIC statement separator : is not configurable. It is part of BASIC itself. DC$ controls only the marker used in label definitions.


Generated Line Numbers

By default, generated BASIC begins at line 10 and increments by 10:

10
20
30
...

The defaults are configurable.

In the legacy awk implementation:

line = 10
incr = 10

In the MBASIC implementation:

STARTLN=10
INCR=10

Generated line numbers are limited to MBASIC's range of 0 through 65529. Python rejects a starting line outside that range and a non-positive increment. All three implementations stop before emitting output if an allocated line would exceed 65529; structured expansion and preserved comments count toward the limit, while preserved blank lines remain unnumbered.

Removing comments, labels, blank lines, or disabled code can cause source line numbers and generated BASIC line numbers to differ substantially.

Warnings therefore report both when useful.

Example:

Warning: duplicate label FOO at source line 17 (120);
first defined at source line 4 (30)

Duplicate and Undefined Labels

Labels are case-insensitive.

Defining the same label more than once produces a warning, including source and generated BASIC locations.

Example:

':foo
...
':FOO

Undefined symbolic references are left unchanged in generated output and produce a warning.

Example:

goto @misspelled

This behavior is intentional. Leaving the unresolved token visible makes mistakes easy to spot rather than silently substituting an arbitrary value.

Repeated references to the same undefined label normally produce only one warning.


Comments

BAL recognizes both traditional BASIC REM comments and apostrophe comments.

Examples:

REM full-line comment

' full-line apostrophe comment

print x   ' trailing comment

Quoted apostrophes are not comments:

print "don't treat this as a comment"

Likewise, label-like text inside a comment is not resolved:

' goto @foo
REM @foo is not a reference here

A REM following a BASIC statement separator is treated as comment-to-end-of-line:

goto @done : REM stop here

Strings

Quoted strings are preserved.

BAL understands doubled quotes inside BASIC strings:

print "He said ""hello"""

Symbolic references, apostrophes, colons, and structured-looking text inside strings are not interpreted as BAL syntax.

For example:

print "goto @foo"
print "THEN!"
print "colon: inside string"

are passed through as strings.


Structured Syntax Errors

LBLBASIC diagnoses structural problems such as:

  • ELSE! without a matching IF
  • more than one ELSE! in the same IF
  • ENDIF! without a matching IF
  • WEND! without a matching WHILE
  • LOOP! without a matching DO
  • mismatched block nesting
  • EXIT! outside a loop
  • CONTINUE! outside a loop
  • unmatched 'ON!
  • unterminated 'OFF!
  • unmatched or unterminated 'OPT! / 'ENDOPT!
  • invalid, duplicate, or conflicting 'CONST! definitions
  • a reached 'ERROR! directive
  • invalid stack declarations or operands
  • generated line numbers beyond MBASIC's limit
  • unterminated structured blocks at end-of-file

For structural errors, translation should be considered unsuccessful.


The Four Distributed Files

lblbasic.py

This is the current reference implementation and executable language specification. Use it with Python 3; it has no third-party dependencies:

python3 lblbasic.py input.bal > output.bas

New language behavior is prototyped and regression-tested here before being ported to the BAL/MBASIC implementation.

lblbasic.awk

This was the original reference implementation and the environment in which the BAL language was first developed.

Use it on a Unix-like system with awk:

awk -f lblbasic.awk input.bal > output.bas

It can also bootstrap the checked-in native implementation:

awk -f lblbasic.awk lblbasic.bal > lblbas.bas

lblbasic.bal intentionally avoids using INCLUDE, CONST, OPT/ENDOPT, ERROR, and STACK/PUSH/POP as directives in its own source, even though the resulting native translator implements them. This keeps the legacy bootstrap viable.

It remains useful on small Unix-like installations and as a parity oracle for the original feature set. It does not implement INCLUDE, CONST, OPT/ENDOPT, ERROR, or STACK/PUSH/POP. New features are not required to be added to it now that the evolving reference implementation has moved to Python.


lblbasic.bal

This is LBLBASIC implemented in BAL itself.

It demonstrates that BAL is usable for writing a nontrivial BASIC program while avoiding raw numeric branch targets.

To convert it to ordinary MBASIC source:

python3 lblbasic.py lblbasic.bal > lblbas.bas

Because lblbasic.bal uses BAL features to implement BAL, lblbasic.py serves as the current stage-0 bootstrap implementation. The awk program can also bootstrap this version of the BAL source.


lblbas.bas

This is the already-converted MBASIC program.

The eight-character basename is intentional for CP/M compatibility.

Run it under Microsoft MBASIC and supply an input .BAL filename and output .BAS filename when prompted.

For example, conceptually:

A>MBASIC LBLBAS
LBLBASIC/MBASIC
Input .BAL file? TEST.BAL
Output .BAS file? TEST.BAS

Exact invocation may vary with the MBASIC version and CP/M environment.

Remember that CP/M tools commonly expect CRLF line endings. If files were created on Unix/Linux, convert them before transferring:

unix2dos lblbasic.bal
unix2dos program.bal

and likewise for generated .BAS files when needed.


Bootstrap Model

LBLBASIC is intentionally developed in stages:

lblbasic.py
     |
     | processes
     v
lblbasic.bal
     |
     v
lblbas.bas
     |
     | runs under MBASIC
     v
other .bal programs

This lets the language be developed in a straightforward modern reference implementation while keeping an implementation that runs on the kind of classic BASIC environment the tool is intended to support.

A useful regression/bootstrap test is to use lblbas.bas under MBASIC to process lblbasic.bal itself and compare that result with the lblbas.bas produced by the Python version.


Design Philosophy

LBLBASIC is not intended to become a full BASIC compiler.

In particular, it deliberately avoids parsing BASIC expressions or implementing operator precedence.

For example, in:

if A=3 AND (B=7 OR C$="X") then!

LBLBASIC treats:

A=3 AND (B=7 OR C$="X")

as opaque BASIC text.

It generates the necessary control-flow scaffolding and leaves expression parsing and evaluation to BASIC.

This keeps the preprocessor small enough to implement in MBASIC itself while still providing a substantial improvement over manually maintaining numeric line targets.


Example Program

':main

x=0

while! x<10
    x=x+1

    if x=3 then!
        continue!
    endif!

    if x=8 then!
        exit!
    endif!

    print x
wend!

do!
    x=x-1

    if x=2 then!
        print "two"
    else!
        print x
    endif!

loop! while x>0

goto @done

'OFF!
':oldcode
print "this entire section is disabled"
goto @missing
'ON!

':done
print "done"
end

Run:

python3 lblbasic.py example.bal > example.bas

The resulting .bas contains only ordinary numbered BASIC and can be transferred to the target system.


File Extensions

The recommended conventions are:

  • .bal — LBLBASIC source
  • .bas — generated ordinary BASIC

The name BAL simply means BASIC with labels.

About

Basic preprocessor to convert structured basic with labels to mbasic for use on old computers

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages