oaknut.basic

The BBC BASIC II tokeniser, de-tokeniser, and line-numbering facade.

Conversion functions

oaknut.basic.tokenise(source, *, start=None, step=None)

Tokenise BBC BASIC II source text into a stored program.

Parameters:
  • source (str) – BBC BASIC source. By default every non-blank line must begin with a line number, as a saved LIST would.

  • start (int | None) – First line number for auto-numbering. Supplying start and/or step turns auto-numbering on (as typing under AUTO would); the source must then have no line numbers. Defaults to 10 when only step is given.

  • step (int | None) – Increment for auto-numbering. Defaults to 10 when only start is given.

Returns:

The tokenised program bytes, terminated by &0D &FF.

Raises:

TokeniseError – The source is malformed — an unnumbered line (or, under auto-numbering, an already-numbered one), a line number out of range or out of order, or a line that tokenises beyond the maximum storable length.

Return type:

bytes

oaknut.basic.detokenise(data, *, dialect=Dialect(name='BBC BASIC II', single_byte=mappingproxy({128: 'AND', 148: 'ABS', 149: 'ACS', 150: 'ADVAL', 151: 'ASC', 152: 'ASN', 153: 'ATN', 198: 'AUTO', 154: 'BGET', 213: 'BPUT', 251: 'COLOUR', 214: 'CALL', 215: 'CHAIN', 189: 'CHR$', 216: 'CLEAR', 217: 'CLOSE', 218: 'CLG', 219: 'CLS', 155: 'COS', 156: 'COUNT', 220: 'DATA', 157: 'DEG', 221: 'DEF', 199: 'DELETE', 129: 'DIV', 222: 'DIM', 223: 'DRAW', 225: 'ENDPROC', 224: 'END', 226: 'ENVELOPE', 139: 'ELSE', 160: 'EVAL', 158: 'ERL', 133: 'ERROR', 197: 'EOF', 130: 'EOR', 159: 'ERR', 161: 'EXP', 162: 'EXT', 227: 'FOR', 163: 'FALSE', 164: 'FN', 229: 'GOTO', 190: 'GET$', 165: 'GET', 228: 'GOSUB', 230: 'GCOL', 147: 'HIMEM', 232: 'INPUT', 231: 'IF', 191: 'INKEY$', 166: 'INKEY', 168: 'INT', 167: 'INSTR(', 201: 'LIST', 134: 'LINE', 200: 'LOAD', 146: 'LOMEM', 234: 'LOCAL', 192: 'LEFT$(', 169: 'LEN', 233: 'LET', 171: 'LOG', 170: 'LN', 193: 'MID$(', 235: 'MODE', 131: 'MOD', 236: 'MOVE', 237: 'NEXT', 202: 'NEW', 172: 'NOT', 203: 'OLD', 238: 'ON', 135: 'OFF', 132: 'OR', 142: 'OPENIN', 174: 'OPENOUT', 173: 'OPENUP', 255: 'OSCLI', 241: 'PRINT', 144: 'PAGE', 143: 'PTR', 175: 'PI', 240: 'PLOT', 176: 'POINT(', 242: 'PROC', 177: 'POS', 248: 'RETURN', 245: 'REPEAT', 246: 'REPORT', 243: 'READ', 244: 'REM', 249: 'RUN', 178: 'RAD', 247: 'RESTORE', 194: 'RIGHT$(', 179: 'RND', 204: 'RENUMBER', 136: 'STEP', 205: 'SAVE', 180: 'SGN', 181: 'SIN', 182: 'SQR', 137: 'SPC', 195: 'STR$', 196: 'STRING$(', 212: 'SOUND', 250: 'STOP', 183: 'TAN', 140: 'THEN', 184: 'TO', 138: 'TAB(', 252: 'TRACE', 145: 'TIME', 185: 'TRUE', 253: 'UNTIL', 186: 'USR', 239: 'VDU', 187: 'VAL', 188: 'VPOS', 254: 'WIDTH', 208: 'PAGE', 207: 'PTR', 209: 'TIME', 210: 'LOMEM', 211: 'HIMEM'}), escape=mappingproxy({})))

De-tokenise a stored BBC BASIC program into source text.

Parameters:
  • data (bytes) – A tokenised program, as stored on disc or in memory, terminated by the &0D &FF end marker.

  • dialect (Dialect) – The BBC BASIC variant whose token tables to use. Defaults to BASIC_II; pass BASIC_V for Archimedes / RISC OS programs so the &C6/&C7/&C8 escape tokens and the re-purposed single-byte tokens decode correctly.

Returns:

The program as numbered source text, lines separated by "\n" with a trailing newline. Empty for an empty program.

Raises:

DetokeniseError – The token stream is malformed — a missing line marker, a truncated header or reference, or an impossible length byte.

Return type:

str

oaknut.basic.detokenise_body(data, *, dialect=Dialect(name='BBC BASIC II', single_byte=mappingproxy({128: 'AND', 148: 'ABS', 149: 'ACS', 150: 'ADVAL', 151: 'ASC', 152: 'ASN', 153: 'ATN', 198: 'AUTO', 154: 'BGET', 213: 'BPUT', 251: 'COLOUR', 214: 'CALL', 215: 'CHAIN', 189: 'CHR$', 216: 'CLEAR', 217: 'CLOSE', 218: 'CLG', 219: 'CLS', 155: 'COS', 156: 'COUNT', 220: 'DATA', 157: 'DEG', 221: 'DEF', 199: 'DELETE', 129: 'DIV', 222: 'DIM', 223: 'DRAW', 225: 'ENDPROC', 224: 'END', 226: 'ENVELOPE', 139: 'ELSE', 160: 'EVAL', 158: 'ERL', 133: 'ERROR', 197: 'EOF', 130: 'EOR', 159: 'ERR', 161: 'EXP', 162: 'EXT', 227: 'FOR', 163: 'FALSE', 164: 'FN', 229: 'GOTO', 190: 'GET$', 165: 'GET', 228: 'GOSUB', 230: 'GCOL', 147: 'HIMEM', 232: 'INPUT', 231: 'IF', 191: 'INKEY$', 166: 'INKEY', 168: 'INT', 167: 'INSTR(', 201: 'LIST', 134: 'LINE', 200: 'LOAD', 146: 'LOMEM', 234: 'LOCAL', 192: 'LEFT$(', 169: 'LEN', 233: 'LET', 171: 'LOG', 170: 'LN', 193: 'MID$(', 235: 'MODE', 131: 'MOD', 236: 'MOVE', 237: 'NEXT', 202: 'NEW', 172: 'NOT', 203: 'OLD', 238: 'ON', 135: 'OFF', 132: 'OR', 142: 'OPENIN', 174: 'OPENOUT', 173: 'OPENUP', 255: 'OSCLI', 241: 'PRINT', 144: 'PAGE', 143: 'PTR', 175: 'PI', 240: 'PLOT', 176: 'POINT(', 242: 'PROC', 177: 'POS', 248: 'RETURN', 245: 'REPEAT', 246: 'REPORT', 243: 'READ', 244: 'REM', 249: 'RUN', 178: 'RAD', 247: 'RESTORE', 194: 'RIGHT$(', 179: 'RND', 204: 'RENUMBER', 136: 'STEP', 205: 'SAVE', 180: 'SGN', 181: 'SIN', 182: 'SQR', 137: 'SPC', 195: 'STR$', 196: 'STRING$(', 212: 'SOUND', 250: 'STOP', 183: 'TAN', 140: 'THEN', 184: 'TO', 138: 'TAB(', 252: 'TRACE', 145: 'TIME', 185: 'TRUE', 253: 'UNTIL', 186: 'USR', 239: 'VDU', 187: 'VAL', 188: 'VPOS', 254: 'WIDTH', 208: 'PAGE', 207: 'PTR', 209: 'TIME', 210: 'LOMEM', 211: 'HIMEM'}), escape=mappingproxy({})))

De-tokenise an unframed line body into text.

Unlike detokenise(), this expects a line body — inline token bytes with no &0D framing and no leading line number, the form BBC Micro Bot / Owlet programs and AUTO-style entry arrive in. It is the text join of oaknut.basic.scan(); when keyword adjacency matters, consume that token stream directly instead.

Parameters:
  • data (bytes) – A line body of inline token bytes.

  • dialect (Dialect) – The BBC BASIC variant whose token tables to use, passed through to scan(). Defaults to BASIC_II.

Returns:

The body as source text, using latin-1/code-point semantics so it round-trips byte-exactly through oaknut.basic.tokenise().

Raises:

DetokeniseError – A &8D line-number reference is truncated.

Return type:

str

oaknut.basic.number_lines(source, *, start=10, step=10)

Prepend ascending line numbers to unnumbered BBC BASIC source.

Each line of source receives a line number, beginning at start and increasing by step, in the manner of the BBC’s AUTO start,step command. Internal references (GOTO, GOSUB, …) are left untouched; the caller is responsible for ensuring they already match the numbering produced here.

Parameters:
  • source (str) – BBC BASIC source text with no line numbers, lines separated by newlines. "\n", "\r" and "\r\n" are all accepted as line terminators.

  • start (int) – Line number given to the first line. Must not be negative.

  • step (int) – Increment between successive line numbers. Must be at least 1.

Returns:

The source with a line number prepended to every line, lines joined by "\n". A trailing newline in source is preserved.

Raises:

ValueErrorstart is negative or step is less than 1.

Return type:

str

Token stream

De-tokenising to text loses keyword adjacency — the body ?Q <PLOT> ?Q renders as ?QPLOT?Q, which the ROM tokeniser re-reads as the variable QPLOT. A consumer that must recover the program (a faithful front end, a re-tokeniser) needs the bytes as a token stream, not text. scan() provides it for an unframed line body, and scan_program() walks the &0D-framed stored form. The text de-tokenisers above are the join of these streams’ values.

oaknut.basic.scan(data, *, base_offset=0, dialect=Dialect(name='BBC BASIC II', single_byte=mappingproxy({128: 'AND', 148: 'ABS', 149: 'ACS', 150: 'ADVAL', 151: 'ASC', 152: 'ASN', 153: 'ATN', 198: 'AUTO', 154: 'BGET', 213: 'BPUT', 251: 'COLOUR', 214: 'CALL', 215: 'CHAIN', 189: 'CHR$', 216: 'CLEAR', 217: 'CLOSE', 218: 'CLG', 219: 'CLS', 155: 'COS', 156: 'COUNT', 220: 'DATA', 157: 'DEG', 221: 'DEF', 199: 'DELETE', 129: 'DIV', 222: 'DIM', 223: 'DRAW', 225: 'ENDPROC', 224: 'END', 226: 'ENVELOPE', 139: 'ELSE', 160: 'EVAL', 158: 'ERL', 133: 'ERROR', 197: 'EOF', 130: 'EOR', 159: 'ERR', 161: 'EXP', 162: 'EXT', 227: 'FOR', 163: 'FALSE', 164: 'FN', 229: 'GOTO', 190: 'GET$', 165: 'GET', 228: 'GOSUB', 230: 'GCOL', 147: 'HIMEM', 232: 'INPUT', 231: 'IF', 191: 'INKEY$', 166: 'INKEY', 168: 'INT', 167: 'INSTR(', 201: 'LIST', 134: 'LINE', 200: 'LOAD', 146: 'LOMEM', 234: 'LOCAL', 192: 'LEFT$(', 169: 'LEN', 233: 'LET', 171: 'LOG', 170: 'LN', 193: 'MID$(', 235: 'MODE', 131: 'MOD', 236: 'MOVE', 237: 'NEXT', 202: 'NEW', 172: 'NOT', 203: 'OLD', 238: 'ON', 135: 'OFF', 132: 'OR', 142: 'OPENIN', 174: 'OPENOUT', 173: 'OPENUP', 255: 'OSCLI', 241: 'PRINT', 144: 'PAGE', 143: 'PTR', 175: 'PI', 240: 'PLOT', 176: 'POINT(', 242: 'PROC', 177: 'POS', 248: 'RETURN', 245: 'REPEAT', 246: 'REPORT', 243: 'READ', 244: 'REM', 249: 'RUN', 178: 'RAD', 247: 'RESTORE', 194: 'RIGHT$(', 179: 'RND', 204: 'RENUMBER', 136: 'STEP', 205: 'SAVE', 180: 'SGN', 181: 'SIN', 182: 'SQR', 137: 'SPC', 195: 'STR$', 196: 'STRING$(', 212: 'SOUND', 250: 'STOP', 183: 'TAN', 140: 'THEN', 184: 'TO', 138: 'TAB(', 252: 'TRACE', 145: 'TIME', 185: 'TRUE', 253: 'UNTIL', 186: 'USR', 239: 'VDU', 187: 'VAL', 188: 'VPOS', 254: 'WIDTH', 208: 'PAGE', 207: 'PTR', 209: 'TIME', 210: 'LOMEM', 211: 'HIMEM'}), escape=mappingproxy({})))

Scan an unframed line body into a stream of Token.

Parameters:
  • data (bytes) – A line body — inline token bytes with no &0D line framing and no leading line number.

  • base_offset (int) – Added to every token’s start, so offsets can be reported against a larger buffer (for example the line body’s position within a whole program).

  • dialect (Dialect) – The BBC BASIC variant whose token tables to use. Defaults to BASIC_II; pass BASIC_V to resolve the &C6/&C7/&C8 two-byte escape tokens and the re-purposed single-byte tokens of the Archimedes language.

Yields:

One Token per keyword, literal run, string literal, and &8D line-number reference, in source order. For a two-byte escape keyword the token’s token is the prefix byte and its start is the prefix’s offset.

Raises:

DetokeniseError – A &8D reference is truncated.

Return type:

Iterator[Token]

oaknut.basic.scan_program(data, *, dialect=Dialect(name='BBC BASIC II', single_byte=mappingproxy({128: 'AND', 148: 'ABS', 149: 'ACS', 150: 'ADVAL', 151: 'ASC', 152: 'ASN', 153: 'ATN', 198: 'AUTO', 154: 'BGET', 213: 'BPUT', 251: 'COLOUR', 214: 'CALL', 215: 'CHAIN', 189: 'CHR$', 216: 'CLEAR', 217: 'CLOSE', 218: 'CLG', 219: 'CLS', 155: 'COS', 156: 'COUNT', 220: 'DATA', 157: 'DEG', 221: 'DEF', 199: 'DELETE', 129: 'DIV', 222: 'DIM', 223: 'DRAW', 225: 'ENDPROC', 224: 'END', 226: 'ENVELOPE', 139: 'ELSE', 160: 'EVAL', 158: 'ERL', 133: 'ERROR', 197: 'EOF', 130: 'EOR', 159: 'ERR', 161: 'EXP', 162: 'EXT', 227: 'FOR', 163: 'FALSE', 164: 'FN', 229: 'GOTO', 190: 'GET$', 165: 'GET', 228: 'GOSUB', 230: 'GCOL', 147: 'HIMEM', 232: 'INPUT', 231: 'IF', 191: 'INKEY$', 166: 'INKEY', 168: 'INT', 167: 'INSTR(', 201: 'LIST', 134: 'LINE', 200: 'LOAD', 146: 'LOMEM', 234: 'LOCAL', 192: 'LEFT$(', 169: 'LEN', 233: 'LET', 171: 'LOG', 170: 'LN', 193: 'MID$(', 235: 'MODE', 131: 'MOD', 236: 'MOVE', 237: 'NEXT', 202: 'NEW', 172: 'NOT', 203: 'OLD', 238: 'ON', 135: 'OFF', 132: 'OR', 142: 'OPENIN', 174: 'OPENOUT', 173: 'OPENUP', 255: 'OSCLI', 241: 'PRINT', 144: 'PAGE', 143: 'PTR', 175: 'PI', 240: 'PLOT', 176: 'POINT(', 242: 'PROC', 177: 'POS', 248: 'RETURN', 245: 'REPEAT', 246: 'REPORT', 243: 'READ', 244: 'REM', 249: 'RUN', 178: 'RAD', 247: 'RESTORE', 194: 'RIGHT$(', 179: 'RND', 204: 'RENUMBER', 136: 'STEP', 205: 'SAVE', 180: 'SGN', 181: 'SIN', 182: 'SQR', 137: 'SPC', 195: 'STR$', 196: 'STRING$(', 212: 'SOUND', 250: 'STOP', 183: 'TAN', 140: 'THEN', 184: 'TO', 138: 'TAB(', 252: 'TRACE', 145: 'TIME', 185: 'TRUE', 253: 'UNTIL', 186: 'USR', 239: 'VDU', 187: 'VAL', 188: 'VPOS', 254: 'WIDTH', 208: 'PAGE', 207: 'PTR', 209: 'TIME', 210: 'LOMEM', 211: 'HIMEM'}), escape=mappingproxy({})))

Walk a &0D-framed stored program, scanning each line body.

Parameters:
  • data (bytes) – A tokenised program as stored on disc or in memory, terminated by the &0D &FF end marker.

  • dialect (Dialect) – The BBC BASIC variant whose token tables to use, passed through to scan(). Defaults to BASIC_II.

Yields:

One LineRecord per line, in storage order, each carrying the line number and its body Token stream.

Raises:

DetokeniseError – The framing is malformed — a missing line marker, a truncated header or reference, or an impossible length byte.

Return type:

Iterator[LineRecord]

class oaknut.basic.Token(kind, value, start, token=None)

One unit of a scanned BBC BASIC line body.

Parameters:
kind

Which TokenKind this is.

Type:

oaknut.basic.scanner.TokenKind

value

The keyword spelling (KEYWORD), the literal run (TEXT), the quoted string including its quotes (STRING), or the decimal line number (LINENUM).

Type:

str

start

Byte offset of the token within the scanned data, shifted by base_offset.

Type:

int

token

The token byte, for a KEYWORD; None otherwise.

Type:

int | None

class oaknut.basic.TokenKind(*values)

The kind of a scanned Token.

KEYWORD = 'KEYWORD'

A keyword token byte, resolved to its spelling via the token table.

TEXT = 'TEXT'

A run of literal bytes — identifiers, numbers, operators, punctuation, spaces, and any unknown high byte — for the consumer to sub-lex. Each byte b appears as chr(b).

STRING = 'STRING'

A "..." string literal, copied verbatim through the closing quote (or the end of the body, if unterminated).

LINENUM = 'LINENUM'

A &8D three-byte line-number reference, decoded to decimal.

class oaknut.basic.LineRecord(line_number, tokens, start)

One line of a scanned stored program.

Parameters:
line_number

The line’s number.

Type:

int

tokens

The line body as a tuple of Token.

Type:

tuple[oaknut.basic.scanner.Token, …]

start

Byte offset of the line’s &0D marker.

Type:

int

Dialects

BBC BASIC II is the BBC Micro’s 8-bit language ROM. BBC BASIC V — the Archimedes / RISC OS ARM BASIC — keeps that token table but re-uses the &C6/&C7/&C8 command bytes as two-byte escape prefixes (the following byte selects an extended keyword such as SUM, RENUMBER, CASE, SYS or ORIGIN) and re-purposes several single-byte slots (&7F becomes OTHERWISE; &C9-&CE become the block keywords WHEN/OF/ENDCASE/ELSE/ENDIF/ENDWHILE). De-tokenising a BASIC V program with the BASIC II tables corrupts every extended keyword, so detokenise(), detokenise_body(), scan() and scan_program() take a dialect argument. BASIC_II is the default everywhere.

class oaknut.basic.Dialect(name, single_byte, escape)

A BBC BASIC variant’s token-to-keyword mapping.

Parameters:
name

Human-readable dialect name (e.g. "BBC BASIC V").

Type:

str

single_byte

Maps a single token byte to its keyword spelling. Covers the whole one-byte keyword range, including the few sub-&80 tokens such as &7F (OTHERWISE).

Type:

collections.abc.Mapping[int, str]

escape

Maps a two-byte escape prefix (&C6/&C7/&C8 in BASIC V) to a table from the following byte to its extended keyword. Empty for dialects without escape tokens.

Type:

collections.abc.Mapping[int, collections.abc.Mapping[int, str]]

oaknut.basic.BASIC_II

The default Dialect: BBC BASIC II, the BBC Micro’s 8-bit language ROM. No escape tokens.

oaknut.basic.BASIC_V

The Dialect for BBC BASIC V — the Archimedes / RISC OS ARM BASIC — with the &C6/&C7/&C8 escape tables and the re-purposed single-byte tokens.

Token table

The BBC BASIC II keyword/token table and flag definitions, for a byte-level consumer that resolves tokens itself. The mapping is exposed read-only; mutating it would corrupt the tokeniser and de-tokeniser.

oaknut.basic.TOKEN_TO_KEYWORD

Read-only proxy of a mapping.

oaknut.basic.KEYWORDS

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.

If the argument is a tuple, the return value is the same object.

oaknut.basic.LINE_NUMBER_TOKEN = 141

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.decode_line_number(payload)

Decode the 3 payload bytes following &8D back to a line number.

Parameters:

payload (bytes) – Exactly the three bytes after the &8D token.

Returns:

The decoded line number.

Raises:

ValueErrorpayload is not exactly three bytes.

Return type:

int

The crunch flag bits, as read from the disassembly’s bit-by-bit dispatch:

oaknut.basic.FLAG_CONDITIONAL = 1

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_MIDDLE = 2

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_START = 4

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_FN_PROC = 8

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_LINE_NUMBER = 16

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_STOP_LINE = 32

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.FLAG_PSEUDO_VAR = 64

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

Data files

The channel-based files BBC BASIC creates with OPENOUT and writes with PRINT# / BPUT#. The module-level open() mirrors the built-in open: a mode string selects a reader, a writer, or a combined read/write object, and accepts a path or an already-open binary stream.

oaknut.basic.datafile.open(file, mode='r', *, encoding='acorn', newline=None, errors='strict')

Open a BBC BASIC data file, styled on the built-in open.

Parameters:
  • file (str | PathLike | IO[bytes]) – A filesystem path, or an already-open binary stream (e.g. io.BytesIO). A path is opened and owned (closed on exit); a stream is borrowed and left open, but is repositioned to suit mode (rewound for read/update, truncated for w, moved to the end for append).

  • mode (str) – One of r (read), w (create/truncate for write), a (append), or the + update variants r+ / w+ / a+ (read and write). Binary is implicit.

  • encoding (str) – Codec for string records; the BBC acorn set by default.

  • newline (str | None) – Newline translation, as for the built-in open. The default (None) translates \n to the Acorn \r on write and any of \r\n / \r / \n to \n on read. "" disables translation.

  • errors (str) – Codec error policy for string records.

Returns:

A BbcBasicDataReader, BbcBasicDataWriter or BbcBasicDataFile according to mode.

Raises:

ValueErrormode or newline is not recognised.

Return type:

BbcBasicDataFileBase

The three classes form a diamond: a shared base carries positioning and lifecycle, the reader and writer add their record vocabularies, and the combined class inherits both for the update modes.

class oaknut.basic.BbcBasicDataReader(stream, *, owns_stream, encoding, newline, errors)

A BBC BASIC data file open for reading (OPENIN).

Parameters:
read()

Read the next tagged record, adapting to its type.

Returns the value as an int, float or str according to the record’s type tag, or None at end of file (so a read loop can be written while (v := f.read()) is not None).

Raises:
Return type:

int | float | str | None

read_int()

Read the next record, which must be an integer.

Raises:
Return type:

int

read_float()

Read the next record, which must be a real. See read_int().

Return type:

float

read_str()

Read the next record, which must be a string. See read_int().

Return type:

str

read_byte()

Read one raw byte (BGET#).

Raises:

EOFError – At end of file.

Return type:

int

read_bytes(size=-1)

Read up to size raw bytes (BGET#), or all remaining if -1.

Follows binary-stream semantics: returns fewer bytes than asked at end of file, and b"" once exhausted (no exception).

Parameters:

size (int)

Return type:

bytes

class oaknut.basic.BbcBasicDataWriter(stream, *, owns_stream, encoding, newline, errors)

A BBC BASIC data file open for writing (OPENOUT / append).

Parameters:
write(value)

Write value, choosing the record type from its Python type.

int → integer record, float → real record, str → string record, bytes/bytearray → raw bytes (untagged, as BPUT# would). bool and any other type raise TypeError; BBC BASIC has no boolean type.

Parameters:

value (int | float | str | bytes | bytearray)

Return type:

None

write_int(value)

Write an integer record. Raises TypeError for a non-int.

Raises:

IntegerRangeError – The value is outside the signed 32-bit range.

Parameters:

value (int)

Return type:

None

write_float(value)

Write a real record. Raises TypeError for a non-float.

Raises:

Float5RangeError – The value is too large for the 5-byte format.

Parameters:

value (float)

Return type:

None

write_str(value)

Write a string record. Raises TypeError for a non-str.

Raises:

StringTooLongError – The encoded string exceeds 255 bytes.

Parameters:

value (str)

Return type:

None

write_bytes(value)

Write raw bytes with no tag or framing (BPUT#).

Parameters:

value (bytes | bytearray)

Return type:

None

write_byte(value)

Write one raw byte (BPUT#).

Raises:
Parameters:

value (int)

Return type:

None

class oaknut.basic.BbcBasicDataFile(stream, *, owns_stream, encoding, newline, errors)

A BBC BASIC data file open for both reading and writing (OPENUP).

Parameters:
class oaknut.basic.BbcBasicDataFileBase(stream, *, owns_stream, encoding, newline, errors)

Common state and positioning for a BBC BASIC data file.

Holds the underlying binary stream, the string encoding and newline policy, and ownership of the stream (a path opened by open() is owned and closed on exit; a caller-supplied stream is borrowed and left open). Subclasses add the read and/or write vocabulary.

Parameters:
close()

Close the file, closing the stream only if this object owns it.

Return type:

None

tell()

Return the current byte position (PTR#).

Return type:

int

seek(position, whence=0)

Set the current byte position (PTR#=); return the new position.

Parameters:
Return type:

int

property extent: int

The length of the file in bytes (EXT#).

at_eof()

Return whether the position is at or past the end (EOF#).

Return type:

bool

A real number is stored in the BBC’s packed 5-byte format. These two functions convert between that format and a Python float in the natural exponent-first byte order, for reuse beyond data files:

oaknut.basic.pack_float5(value)

Encode a Python float into five exponent-first packed bytes.

The 32-bit significand is rounded to nearest, ties to even. A tie that rounds the significand up to 1.0 carries into the exponent rather than overflowing. Magnitudes below the smallest normal underflow to zero (the format has no denormals).

Parameters:

value (float) – A finite Python float.

Returns:

Five bytes in natural (exponent-first) order. 0.0 (and -0.0) encode to five zero bytes.

Raises:

Float5RangeError – If value is non-finite or its magnitude is too large for the excess-128 exponent.

Return type:

bytes

oaknut.basic.unpack_float5(packed)

Decode five exponent-first packed bytes into a Python float.

Parameters:

packed (bytes) – Exactly five bytes in natural (exponent-first) order.

Returns:

The decoded value. A zero exponent decodes to 0.0 regardless of the remaining bytes.

Raises:

ValueError – If packed is not exactly five bytes long.

Return type:

float

Constants

Canonical load addresses for a tokenised program on each host machine — the addresses *SAVE uses by default:

oaknut.basic.BBC_BASIC_LOAD_ADDRESS = 6400

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.ELECTRON_BASIC_LOAD_ADDRESS = 3584

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

The default AUTO numbering parameters, shared by number_lines() and tokenise():

oaknut.basic.DEFAULT_LINE_NUMBER = 10

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

oaknut.basic.DEFAULT_LINE_STEP = 10

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int(‘0b100’, base=0) 4

Exceptions

Every error derives from BASICError, itself a oaknut.exception.DataError. The two families mirror the two directions; see Error handling for the hierarchy and how to catch them.

exception oaknut.basic.BASICError(*args, exit_code=None)

Base for every BBC BASIC tokeniser and de-tokeniser error.

Parameters:
  • args (object)

  • exit_code (Optional[ExitCode])

Return type:

None

Tokenising — source text in:

exception oaknut.basic.TokeniseError(message, *, line_index, line_text)

Base for an error in BBC BASIC source being tokenised.

Parameters:
  • message (str)

  • line_index (int)

  • line_text (str)

Return type:

None

line_index

1-based position of the offending line in the source.

line_text

The offending line, verbatim (no trailing newline).

exception oaknut.basic.UnnumberedLineError(line_index, line_text)

A source line has no leading line number.

Raised when tokenising without auto-numbering: every line must begin with a line number (as a saved LIST would).

Parameters:
  • line_index (int)

  • line_text (str)

Return type:

None

exception oaknut.basic.AlreadyNumberedError(line_index, line_number, line_text)

Auto-numbering was requested but the source is already numbered.

Parameters:
  • line_index (int)

  • line_number (int)

  • line_text (str)

Return type:

None

line_number

The line number already present on the line.

exception oaknut.basic.LineNumberRangeError(line_index, line_number, line_text)

A line number is outside the valid range.

Parameters:
  • line_index (int)

  • line_number (int)

  • line_text (str)

Return type:

None

line_number

The out-of-range line number.

exception oaknut.basic.LineNumberOrderError(line_index, line_number, previous_line_number, line_text)

A line number does not exceed the line before it.

BBC BASIC stores lines in strictly ascending order; a listing that repeats or rewinds a line number could not have come from LIST.

Parameters:
  • line_index (int)

  • line_number (int)

  • previous_line_number (int)

  • line_text (str)

Return type:

None

line_number

The offending line number.

previous_line_number

The line number of the preceding line.

exception oaknut.basic.LineTooLongError(line_index, line_number, length, line_text)

A tokenised line exceeds the maximum storable length.

Parameters:
  • line_index (int)

  • line_number (int)

  • length (int)

  • line_text (str)

Return type:

None

line_number

The BASIC line number that overflowed.

length

The tokenised body length in bytes.

De-tokenising — token stream in:

exception oaknut.basic.DetokeniseError(message, *, offset)

Base for an error in a tokenised program being de-tokenised.

Parameters:
Return type:

None

offset

Byte offset into the program where the fault was found.

exception oaknut.basic.MissingLineMarkerError(offset, found)

A line did not begin with the expected &0D marker.

Parameters:
Return type:

None

found

The byte found where the marker was expected.

exception oaknut.basic.TruncatedProgramError(offset, detail)

The token stream ended in the middle of a structure.

Parameters:
Return type:

None

detail

What was being read when the bytes ran out.

exception oaknut.basic.InvalidLineLengthError(offset, length)

A line’s length byte is impossible.

Parameters:
Return type:

None

length

The offending length byte.

Data files — reading and writing records:

exception oaknut.basic.DataFileError(message, *, offset=None)

Base for a fault reading or writing a BBC BASIC data file.

Parameters:
  • message (str)

  • offset (int | None)

Return type:

None

offset

Byte offset of the record where the fault was found, or None when the fault is not tied to a position.

exception oaknut.basic.UnknownTagError(tag, offset)

A tagged record began with a byte that is not a known type tag.

A PRINT# record starts with &00 (string), &40 (integer) or &FF (real); anything else means the stream is not tagged data at this point (often raw BPUT# bytes read as if tagged).

Parameters:
Return type:

None

tag

The offending tag byte.

exception oaknut.basic.DataFileTypeMismatchError(expected, tag, offset)

A typed read found a record of a different type.

Mirrors BASIC’s Type mismatch on INPUT#: the record’s tag did not match the type the caller asked for. The stream position is left at the start of the record so the caller can re-read it.

Parameters:
Return type:

None

expected

Human-readable name of the requested type.

tag

The tag byte actually found.

exception oaknut.basic.TruncatedRecordError(offset, detail)

A tagged record ran past the end of the stream.

Distinct from a clean end of file (which a typed read signals with EOFError): the tag, or part of the payload, was present but the rest of the record was missing — the stream is corrupt.

Parameters:
Return type:

None

detail

What was being read when the bytes ran out.

exception oaknut.basic.IntegerRangeError(value)

An integer is outside the signed 32-bit range a BBC integer holds.

Parameters:

value (int)

Return type:

None

value

The offending integer.

exception oaknut.basic.StringTooLongError(length)

A string is longer than the single length byte can describe.

A PRINT# string is prefixed by one length byte, so it can hold at most 255 characters.

Parameters:

length (int)

Return type:

None

length

The offending string length.

exception oaknut.basic.Float5RangeError(value)

A Python float is too large for the 5-byte BBC REAL format.

The packed format carries an excess-128 exponent, so the largest representable magnitude is just under 2**127. Underflow is not an error — the format has no denormals, so tiny magnitudes flush to zero — but overflow (and any non-finite value) cannot be stored.

Parameters:

value (float)

Return type:

None

value

The offending Python float.