module Z80::Program::Macros

Z80 Macros

A few handy macros.

Public Instance Methods

cp16n(th, tl, value, jr_msb_c: nil, jr_msb_nz: :eoc) click to toggle source

Compares a pair of registers th|tl with a value as unsigned 16-bit integers.

Provide value as an integer or a label.

CF, ZF = (th|tl - value)

CF = 1 if th|tl <  value
CF = 0 if th|tl >= value
ZF = 0 if th|tl <> value
ZF = 1 if th|tl == value

Options:

  • jr_msb_c: provide a label if you want to jump immediately after th < (value >> 8) or :ret symbol to return from a subroutine.

  • jr_msb_nz: provide a label if you want to jump immediately after th <> (value >> 8) or :ret symbol to return from a subroutine. By default jumps to eoc of the cp16n code, so flags can be examined later.

Example:

cp16n  h,l, foo, jr_msb_c: less_than_foo
jr C, less_than_foo

Modifies: af.

# File lib/z80/macros.rb, line 205
def cp16n(th, tl, value, jr_msb_c: nil, jr_msb_nz: :eoc)
        cp16r(th, tl, value>>8, value, jr_msb_c:jr_msb_c, jr_msb_nz:jr_msb_nz)
end
cp16r(th, tl, sh, sl, jr_msb_c: nil, jr_msb_nz: :eoc) click to toggle source

Compares a pair of registers th|tl with another pair sh|sl as unsigned 16-bit integers.

CF, ZF = (th|tl - +sh+|+sl+)

CF = 1 if th|tl <  sh|sl
CF = 0 if th|tl >= sh|sl
ZF = 0 if th|tl <> sh|sl
ZF = 1 if th|tl == sh|sl

Options:

  • jr_msb_c: provide a label if you want to jump immediately after th < sh or :ret symbol to return from a subroutine.

  • jr_msb_nz: provide a label if you want to jump immediately after th <> sh or :ret symbol to return from a subroutine. By default jumps to eoc of the cp16r code, so flags can be examined later.

Example:

cp16r  h,l, d,e, jr_msb_nz: not_equal
jr NZ, not_equal

Modifies: af.

# File lib/z80/macros.rb, line 244
def cp16r(th, tl, sh, sl, jr_msb_c: nil, jr_msb_nz: :eoc)
        raise ArgumentError, "only th can be the accumulator" if [tl, sh, sl].include?(a)
        isolate do |eoc|
                jr_msb_nz = eoc if jr_msb_nz == :eoc
                                                ld   a, th unless th == a
                        if sh == 0
                                        ora  a
                        else
                                        cp   sh
                                case jr_msb_c
                                when :ret
                                        ret  C
                                else
                                        jr   C, jr_msb_c
                                end unless jr_msb_c.nil?
                        end
                        case jr_msb_nz
                        when :ret
                                ret  NZ
                        else
                                jr   NZ, jr_msb_nz
                        end
                                ld   a, tl
                                cp   sl
        end
end
cp16rr(tt, ss, jr_msb_c: nil, jr_msb_nz: :eoc) click to toggle source

Compares a pair of 16-bit registers tt with ss as unsigned integers.

A sugar for:

cp16r(th,tl, sh,sl, ...)

See: Macros#cp16r.

# File lib/z80/macros.rb, line 215
def cp16rr(tt, ss, jr_msb_c: nil, jr_msb_nz: :eoc)
        raise ArgumentError, "tt must be a 16-bit register" unless !pointer?(tt) && register?(tt) && !tt.bit8?
        raise ArgumentError, "ss must be a 16-bit register" unless !pointer?(ss) && register?(ss) && !ss.bit8?
        th, tl = tt.split
        sh, sl = ss.split
        cp16r(th, tl, sh, sl, jr_msb_c:jr_msb_c, jr_msb_nz:jr_msb_nz)
end
ld16(aa, bb) click to toggle source

Loads a content of the 16-bit register bb into the 16-bit register aa.

A sugar for two 8-bit ld instructions.

Example:

ld16  bc, hl
# File lib/z80/macros.rb, line 169
def ld16(aa, bb)
        unless [bc, de, hl, ix, iy].include?(aa) and [bc, de, hl, ix, iy].include?(bb)
                raise ArgumentError, "Use one of: bc de hl ix iy registers in ld16"
        end
        raise ArgumentError, "Registers must be different" if aa == bb
        ah, al = aa.split
        bh, bl = bb.split
        isolate do
                ld  al, bl
                ld  ah, bh
        end
end
macro(name, *registers, **nsopts, &mblock) click to toggle source

A convenient method to create local macros.

Give a name (Symbol) to your macro, an optional list of registers to push before and pop after code and a block of code. See Macros.with_saved for explanation of registers arguments. The block will receive eoc label (see Program.ns) and any argument you pass when calling a macro.

If you want your macros being exportable, do not use this method. Instead create module `Macros' inside your program class and define methods there. Remember to wrap the code in Program.ns, or better yet: Program.isolate.

Macros and labels share the same namespace, however labels can be nested, but macros can't.

Unlike labels, macros must be defined before being used.

Note

Be carefull with ret instruction if you used registers. You can use ret: true option instead, see: Macros.with_saved.

Example:

macro :cp16 do |eoc, aa, bb|
  ah, al = aa.split
  bh, bl = bb.split
           ld  a, ah
           cp  bh
           jr  NZ, eoc
           ld  a, al
           cp  bl
end

cp16 bc, hl
jr   C, bc_less_than_hl
# File lib/z80/macros.rb, line 38
def macro(name, *registers, **nsopts, &mblock)
        name = name.to_sym if name.is_a?(String)
        raise Syntax, "A macro must have a name" unless name.is_a?(Symbol)
        raise Syntax, "Macros may only be defined in the program context." if @contexts.size > 1
        raise Syntax, "A label with the same name: \"#{name}\" exists." if @labels.has_key?(label.to_s)
        m = lambda do |*args, &block|
                if args.first.is_a?(Symbol)
                        n = args.shift
                end
                if registers.empty?
                        ns(n, **nsopts) do |eoc|
                                mblock.call eoc, *args, &block
                        end
                else
                        lbl = with_saved(*registers, **nsopts) do |eoc|
                                mblock.call eoc, *args, &block
                        end
                        lbl = self.define_label n, lbl if n
                        lbl
                end
        end
        define_singleton_method(name.to_sym, &m)
end
with_saved(*registers, **opts, &block) click to toggle source

Adds a code that pushes specified registers on a machine stack, code from block within a namespace and code that pops registers in reverse order. Returns a label pointing to the beginning of the pushes. You may optionally pass a name for the returned label to be named.

registers should be one of:

  • a 16bit register to push on the stack and pop after code from block, except sp obviously,

  • :exx symbol to evaluate exx instruction,

  • :ex_af symbol to evaluate ex af, af instruction,

  • :all symbol evaluates to: af, bc, de, hl, ix, iy

  • :all_but_ixiy symbol evaluates to: af, bc, de, hl

  • :ixiy symbol evaluates to: ix, iy

opts may be one of:

  • :ret if the ret instruction should be added after the registers have been restored. In this instance jumping to eoc will effectively return from the subroutine. If :ret => :after_ei the ei instruction will be added before ret.

Other accepted values for ret: option are:

  • :reti: the reti instruction will be added instead of ret.

  • :retn: the retn instruction will be added instead of ret.

  • :after_ei: the ei instruction will be added before ret.

  • :ei_reti: the ei instruction will be added before reti.

Evaluates your block with Program.ns. All other opts are being passed along to Program.ns.

Examples:

with_saved af, de do |eoc|
  # ... do something with af and de
end

with_saved af, hl, :exx, hl, :exx, ret: true do |eoc|
  # ... saves and restores af, hl, hl' and swaps shadow registers back before
  # adds `ret' instruction after restoring all the registers
end

with_saved :bar, :all, :exx, :ex_af, :all_but_ixiy, inherit: foo, isolate: true do |eoc|
  # saves and restores: af, bc, de, hl, ix, iy, af', bc', de', hl'
  # creates an isolated namespace with inherited `foo' label
  # returns "bar" namespace label addressing beginning of the pushes
end

Be carefull with ret instruction in a provided block. Remember to pop registers first or use :ret option instead.

# File lib/z80/macros.rb, line 107
def with_saved(*registers, **opts, &block)
        name = nil
        with_return = opts.delete :ret
        registers.map!.with_index do |rr, i|
                case rr
                when :all
                        [af, bc, de, hl, ix, iy]
                when :no_ixy, :no_ixiy, :all_but_ixy, :all_but_ixiy, :afbcdehl
                        [af, bc, de, hl]
                when :ixy, :ixiy
                        [ix, iy]
                when :ex_af, :ex_af_af, :exx
                        rr
                when Symbol, String
                        if i == 0
                                name = rr
                                []
                        else
                                rr
                        end
                else
                        rr
                end
        end.flatten!
        saver = proc do |rr, instr|
                case rr
                when af, bc, de, hl, ix, iy
                        self.send instr, rr
                when :exx
                        exx
                when :ex_af, :ex_af_af
                        ex af, af
                else
                        raise ArgumentError, "arguments must be one of: af, bc, de, hl, ix, iy, :exx or :ex_af, got: #{rr.inspect}"
                end
        end
        ns(name, **opts) do
                registers.each{|rr| saver[rr, :push]}
                ns(**opts, merge: true, &block)
                registers.reverse_each{|rr| saver[rr, :pop]}
                if with_return
                        ei if [:after_ei, :ei, :ei_ret, :ei_reti].include?(with_return)
                        case with_return
                        when :reti, :ei_reti
                                reti
                        when :retn
                                retn
                        when true, :after_ei, :ei, :ei_ret, :ret
                                ret
                        else
                                raise ArgumentError, "unrecognized value of :ret option: #{with_return.inspect}"
                        end
                end
        end
end