module ZXLib::Basic

A module with ZX Spectrum's BASIC program utilities.

SE BASIC extensions are supported.

See: ZXLib::Basic::Program, ZXLib::Basic::Vars, ZXLib::Basic::Variable

Example:

require 'zxlib/basic'
include ZXLib

program = Basic.parse_source <<-EOB
  10 LET a$="Hello World!"
  20 PRINT a$
EOB
program.start = 10
program.save_tap 'helloworld.tap'

program = Basic.parse_source '10 PRINT s$'
program.start = 10
program.vars << Basic::Variable.new_string('s$', '`FLASH 1`""`INK 1`Hello World!`INK 0`""`FLASH 0`')
program.save_tap 'helloworld.tap', append: true
puts program.vars

chunk = Z80::TAP.read_chunk('helloworld.tap')
program = Basic.from_tap_chunk(chunk)
puts program
source = program.to_source

Constants

ARROWS
CHAR_CODES
CHAR_TABLE
COLOR_CTRL
CTRL_CODES
CURSOR_CTRL
DEF_FN_VAR_PLACEHOLDER
ESCAPE_CODES
ESCAPE_CODES_ASCII_ONLY
KEYWORDS
KEYWORD_CODES
KEYWORD_START_CODE
NON_ASCII_ESCAPE_TOKENS
PRINTABLE_CHARS
SE_ALIASES
SE_KEYWORDS
SE_NEW_KEYWORDS
SE_NEW_KEYWORDS_END
STATEMENTS_AS_EXPRESSIONS_CODES
STATEMENT_START_CODE

Public Class Methods

from_program_data(data, prog_length=nil, start:nil) click to toggle source

Creates a Basic::Program instance from a ZX Spectrum's raw binary data.

The binary data may be a snapshot of ZX Spectrum's memory (PROG + VARS) or taken from a TAP chunk.

Provide program data as a binary (ASCII-8BIT) string. Provide prog_length argument to indicate the size of the program itself. Any data after prog_length bytes will be interpreted as program variables.

Additionally :start argument may be provided to indicate the starting line of a program. This information will be used when saving program as a TAP file.

# File lib/zxlib/basic.rb, line 456
def from_program_data(data, prog_length=nil, start:nil)
        if prog_length.nil?
                prog_length = data.bytesize
        else
                raise "program size must not exceed data size" if prog_length > data.bytesize
        end
        prog, vars = data.unpack("a#{prog_length}a*")
        lines = []
        while true
                no, size, prog = prog.unpack("S>S<a*")
                break if no.nil?
                body, eol, prog = prog.unpack("a#{size - 1}Ca*")
                raise "Invalid program line" unless eol == 13
                lines << Line.new(no, body)
        end
        Program.new lines, vars, start
end
from_tap_chunk(chunk) click to toggle source

Creates a Basic::Program or a Basic::Variable depending on the type of the chunk. The chunk should be a Z80::TAP::HeaderBody instance obtained from e.g. Z80::TAP.read_chunk.

# File lib/zxlib/basic.rb, line 476
def from_tap_chunk(chunk)
        if chunk.program?
                from_program_data chunk.body.data, chunk.header.p2, start: chunk.header.p1
        elsif chunk.array?
                data = [chunk.header.array_head, chunk.header.length, chunk.body.data].pack('Cva*')
                Variable.from_data data
        else
                raise "expected a program, a character array or a number array chunk"
        end
end
parse_source(source, start:nil) click to toggle source

Creates a Basic::Program from a BASIC program text.

The source should be an UTF-8 encoded string.

Each new line separator terminates each BASIC line. The line may start with a decimal indicating the line number but doesn't have to. In this instance the last line number + 1 will be used. The line numbers must be ascending, must not be negative or larger than 9999.

The text is being decoded according to special character conversion rules. For details please consult Basic::Program#to_source.

Note:

This method doesn't interpret the BASIC program. A lot of nonsense will be accepted. However the program text is interpreted using a Basic::Tokenizer and some simple heuristics, mainly to ensure a proper syntax of strings, numbers and some specific expressions:

  • All numbers outside of string literals are followed by a character code 14 and 5 bytes of their internal representation in the FP format (see ZXLib::Math). This also applies to SE BASIC hexadecimal and octal literals.

  • Literal strings are being tracked, ensuring they are properly closed.

  • Opened and closed parentheses are counted ensuring they are properly balanced.

  • A statement keyword is expected (except spaces, control characters and colons) at the beginning of each line, after the colon character or after a THEN keyword. In these instances only the statement keywords are being accepted.

  • Inside parentheses the colon character or a THEN keyword is forbidden.

  • If a statement keyword is not expected only the keywords that may be used in expression context are converted to ZX BASIC keywords.

  • After every argument of a DEF FN header list a character code 14 and 5 placeholder bytes are being added.

  • Argument after the BIN keyword will be interpreted as a binary number.

  • After the REM statement most of the rules are being relaxed until the end of the line.

  • A literal 7 or 8 after the SE BASIC's DIR statement will be interpreted as a single character argument and not a number.

  • In addition to all ZX Spectrum BASIC keywords, the SE BASIC keywords will be also recognized and parsed accordingly. See Basic::Program#to_source for the list of those keywords.

Additionally :start argument may be provided to indicate a starting line of a program. This information will be used when saving program as a TAP file.

# File lib/zxlib/basic.rb, line 533
def parse_source(source, start:nil)
        unless source.force_encoding(Encoding::UTF_8).valid_encoding?
                raise "invalid program source encoding, expecting: UTF-8"
        end
        last_line_no = -1
        lines = []
        source.each_line.with_index do |line_text, line_index|
                line_text.chomp!
                line = Line.parse_source_line(line_text, last_line_no, line_index + 1)
                last_line_no = line.line_no
                lines << line
        end
        Program.new(lines, Vars.new, start)
end
read_source(filename, **opts) click to toggle source

Creates a Basic::Program from a BASIC text file.

See parse_source for details.

# File lib/zxlib/basic.rb, line 551
def read_source(filename, **opts)
        parse_source IO.read(filename, encoding: Encoding::UTF_8), **opts
end
read_tap(filename, **opts) click to toggle source

Creates a Basic::Program or a Basic::Variable from a TAP file.

See Z80::TAP.read_chunk for arguments description.

# File lib/zxlib/basic.rb, line 490
def read_tap(filename, **opts)
        Z80::TAP.read_chunk(filename, **opts) { |chunk| from_tap_chunk(chunk) }
end