Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.25.x' ]
go-version: [ '1.27.x' ]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2026-09-03

### Changed
- **Internal simplification**: the structured decode types (`Operand`, `Register`, `EffectiveAddress`, …) are now defined once in `internal/decoders` and re-exported from the public package via type aliases, removing ~150 lines of value-copying glue. No API or behavior change.
- Consolidated duplicated instruction decoders (immediate ALU ops, address-register ALU ops, unary EA ops, predecrement/postincrement operands) behind shared helpers. Non-test code shrinks from ~2270 to ~2000 lines.
- **Minimum Go version raised to 1.27.** Adopted the `new(expr)` builtin and `range`-over-int; `DisassembleRange` now preallocates its result slice.
- Removed a stray root `go.yml` that duplicated the real CI workflow.

## [1.0.3] - 2026-04-03

### Fixed
- **PC-relative symbolizer regression**: restored correct PC-relative target resolution.
- CI build fix.

## [1.0.2] - 2026-04-03

### Fixed
Expand Down
141 changes: 10 additions & 131 deletions disasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"strings"

"github.com/jenska/m68kdasm/internal/decoders"
)
Expand Down Expand Up @@ -81,7 +82,9 @@ func DisassembleRange(data []byte, startAddress uint32) ([]Instruction, error) {
}

func DisassembleRangeWithOptions(data []byte, startAddress uint32, opts DecodeOptions) ([]Instruction, error) {
var instructions []Instruction
// Every instruction is at least one 16-bit word, so len(data)/2 is an upper
// bound on the instruction count.
instructions := make([]Instruction, 0, len(data)/2)
offset := 0

for offset < len(data) {
Expand Down Expand Up @@ -119,27 +122,7 @@ func decodeInstruction(initial []byte, address uint32, reader addressReader, opt
opcode := binary.BigEndian.Uint16(data[:2])
decoder := decoders.FindDecoder(opcode)
if decoder == nil {
return finalizeInstruction(&decoders.Instruction{
Address: address,
Opcode: opcode,
Mnemonic: "DC.W",
Operands: fmt.Sprintf("$%04X", opcode),
Size: 2,
Bytes: data[:2],
Metadata: decoders.Metadata{
Mnemonic: "DC.W",
MnemonicBase: "DC",
SizeSuffix: "W",
Operands: []decoders.Operand{
{
Text: fmt.Sprintf("$%04X", opcode),
Kind: decoders.OperandKindImmediate,
Immediate: &decoders.ImmediateValue{Value: uint32(opcode), Signed: int32(int16(opcode)), Size: 2},
},
},
ImmediateValues: []decoders.ImmediateValue{{Value: uint32(opcode), Signed: int32(int16(opcode)), Size: 2}},
},
}, opts), nil
return finalizeInstruction(decoders.DecodeUnknown(data, address, opcode), opts), nil
}

for {
Expand Down Expand Up @@ -210,7 +193,7 @@ func finalizeInstruction(decoderInst *decoders.Instruction, opts DecodeOptions)
Size: decoderInst.Size,
Bytes: append([]byte(nil), decoderInst.Bytes...),
ExtensionWords: append([]uint16(nil), decoderInst.ExtensionWords...),
Metadata: convertMetadata(decoderInst.Metadata),
Metadata: decoderInst.Metadata,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve non-nil metadata slices

When decoding instructions with no immediate values, assigning the internal metadata directly retains its nil ImmediateValues slice; operand-less instructions such as NOP also retain nil Operands. The removed conversion always allocated zero-length slices, so JSON output changes from [] to null, which breaks consumers validating these fields as arrays and contradicts the stated no-behavior-change intent. Normalize these slices before returning the public instruction.

Useful? React with 👍 / 👎.

}

if opts.Symbolizer != nil && len(inst.Metadata.Operands) > 0 {
Expand All @@ -220,105 +203,12 @@ func finalizeInstruction(decoderInst *decoders.Instruction, opts DecodeOptions)
return inst
}

func convertMetadata(meta decoders.Metadata) DecodeMetadata {
converted := DecodeMetadata{
Mnemonic: meta.Mnemonic,
MnemonicBase: meta.MnemonicBase,
SizeSuffix: meta.SizeSuffix,
BranchTarget: cloneUint32Ptr(meta.BranchTarget),
ImmediateValues: make([]ImmediateValue, len(meta.ImmediateValues)),
Operands: make([]Operand, len(meta.Operands)),
}
for i, imm := range meta.ImmediateValues {
converted.ImmediateValues[i] = ImmediateValue{Value: imm.Value, Signed: imm.Signed, Size: imm.Size}
}
for i, operand := range meta.Operands {
converted.Operands[i] = convertOperand(operand)
}
return converted
}

func convertOperand(operand decoders.Operand) Operand {
converted := Operand{
Text: operand.Text,
Kind: OperandKind(operand.Kind),
RegisterList: append([]string(nil), operand.RegisterList...),
BranchTarget: cloneUint32Ptr(operand.BranchTarget),
}
if operand.Register != nil {
converted.Register = &Register{
Kind: RegisterKind(operand.Register.Kind),
Number: operand.Register.Number,
}
}
if operand.Immediate != nil {
converted.Immediate = &ImmediateValue{
Value: operand.Immediate.Value,
Signed: operand.Immediate.Signed,
Size: operand.Immediate.Size,
}
}
if operand.EffectiveAddress != nil {
ea := &EffectiveAddress{
Kind: EffectiveAddressKind(operand.EffectiveAddress.Kind),
Mode: operand.EffectiveAddress.Mode,
Register: operand.EffectiveAddress.Register,
Displacement: cloneInt32Ptr(operand.EffectiveAddress.Displacement),
AbsoluteAddress: cloneUint32Ptr(operand.EffectiveAddress.AbsoluteAddress),
ResolvedAddress: cloneUint32Ptr(operand.EffectiveAddress.ResolvedAddress),
}
if operand.EffectiveAddress.Base != nil {
ea.Base = &Register{
Kind: RegisterKind(operand.EffectiveAddress.Base.Kind),
Number: operand.EffectiveAddress.Base.Number,
}
}
if operand.EffectiveAddress.Immediate != nil {
ea.Immediate = &ImmediateValue{
Value: operand.EffectiveAddress.Immediate.Value,
Signed: operand.EffectiveAddress.Immediate.Signed,
Size: operand.EffectiveAddress.Immediate.Size,
}
}
if operand.EffectiveAddress.Index != nil {
ea.Index = &IndexRegister{
Register: Register{
Kind: RegisterKind(operand.EffectiveAddress.Index.Register.Kind),
Number: operand.EffectiveAddress.Index.Register.Number,
},
Size: operand.EffectiveAddress.Index.Size,
}
}
converted.EffectiveAddress = ea
}
return converted
}

func cloneUint32Ptr(v *uint32) *uint32 {
if v == nil {
return nil
}
cloned := *v
return &cloned
}

func cloneInt32Ptr(v *int32) *int32 {
if v == nil {
return nil
}
cloned := *v
return &cloned
}

func formatOperands(operands []Operand, symbolizer Symbolizer) string {
rendered := make([]string, 0, len(operands))
for _, operand := range operands {
rendered = append(rendered, formatOperand(operand, symbolizer))
}
if len(rendered) == 0 {
return ""
rendered := make([]string, len(operands))
for i, operand := range operands {
rendered[i] = formatOperand(operand, symbolizer)
}
return joinOperands(rendered)
return strings.Join(rendered, ", ")
}

func formatOperand(operand Operand, symbolizer Symbolizer) string {
Expand All @@ -341,14 +231,3 @@ func formatOperand(operand Operand, symbolizer Symbolizer) string {
}
return operand.Text
}

func joinOperands(parts []string) string {
if len(parts) == 0 {
return ""
}
out := parts[0]
for _, part := range parts[1:] {
out += ", " + part
}
return out
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module github.com/jenska/m68kdasm

go 1.26
go 1.27

require github.com/jenska/m68kasm v1.3.1
24 changes: 0 additions & 24 deletions go.yml

This file was deleted.

27 changes: 12 additions & 15 deletions internal/decoders/addressing.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Mode: mode,
Register: reg,
Base: &Register{Kind: RegisterKindAddress, Number: reg},
Displacement: int32Ptr(int32(displacement)),
Displacement: new(int32(displacement)),
}), nil

case 6: // Address Register Indirect with Index
Expand All @@ -97,7 +97,7 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Mode: mode,
Register: reg,
Base: &Register{Kind: RegisterKindAddress, Number: reg},
Displacement: int32Ptr(int32(displacement)),
Displacement: new(int32(displacement)),
Index: &IndexRegister{
Register: Register{Kind: parseIndexRegisterKind(indexType), Number: indexReg},
Size: string(indexSize),
Expand All @@ -118,8 +118,8 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Kind: EAKindAbsoluteShort,
Mode: mode,
Register: reg,
AbsoluteAddress: uint32Ptr(absolute),
ResolvedAddress: uint32Ptr(absolute),
AbsoluteAddress: new(absolute),
ResolvedAddress: new(absolute),
}), nil

case 1: // Absolute Long Address
Expand All @@ -132,8 +132,8 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Kind: EAKindAbsoluteLong,
Mode: mode,
Register: reg,
AbsoluteAddress: uint32Ptr(addr),
ResolvedAddress: uint32Ptr(addr),
AbsoluteAddress: new(addr),
ResolvedAddress: new(addr),
}), nil

case 2: // Program Counter with Displacement
Expand All @@ -150,7 +150,7 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Mode: mode,
Register: reg,
Base: &Register{Kind: RegisterKindPC},
Displacement: int32Ptr(int32(displacement)),
Displacement: new(int32(displacement)),
}), nil

case 3: // Program Counter with Index
Expand All @@ -165,7 +165,7 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Mode: mode,
Register: reg,
Base: &Register{Kind: RegisterKindPC},
Displacement: int32Ptr(int32(displacement)),
Displacement: new(int32(displacement)),
Index: &IndexRegister{
Register: Register{Kind: parseIndexRegisterKind(indexType), Number: indexReg},
Size: string(indexSize),
Expand All @@ -186,9 +186,7 @@ func decodeAddressingMode(data []byte, mode, reg uint8, operandSize int) (string
Register: reg,
Immediate: immediatePtr(value, operandSize),
}), nil
case 1:
fallthrough
case 2:
case 1, 2:
if err := requireLength(data, 2, "immediate"); err != nil {
return "", 0, Operand{}, err
}
Expand Down Expand Up @@ -249,14 +247,13 @@ func formatImmediateForMOVEQ(value int32) string {
// decodeIndexWord extracts index register, type, size, and displacement from index word
func decodeIndexWord(indexWord uint16) (indexType string, indexReg, indexSize uint8, displacement int8) {
indexType = "D"
if (indexWord>>15)&0x1 == 1 {
if indexWord&0x8000 != 0 {
indexType = "A"
}
indexReg = uint8((indexWord >> 12) & 0x7)
if (indexWord>>11)&0x1 == 1 {
indexSize = 'W'
if indexWord&0x0800 != 0 {
indexSize = 'L'
} else {
indexSize = 'W'
}
displacement = int8(indexWord & 0xFF)
return
Expand Down
26 changes: 2 additions & 24 deletions internal/decoders/arithmetic.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import "fmt"
// ADD Format: 1101 ddd ooo sss rrr
func decodeADD(data []byte, opcode uint16, inst *Instruction) error {
if isAddressRegisterArithmetic(opcode) {
return decodeAddressRegisterArithmetic("ADD", data, opcode, inst)
return decodeAddressRegisterOp("ADD", data, opcode, inst)
}
return decodeDirectedBinaryOp("ADD", data, opcode, inst)
}
Expand All @@ -15,7 +15,7 @@ func decodeADD(data []byte, opcode uint16, inst *Instruction) error {
// SUB Format: 1001 ddd ooo sss rrr
func decodeSUB(data []byte, opcode uint16, inst *Instruction) error {
if isAddressRegisterArithmetic(opcode) {
return decodeAddressRegisterArithmetic("SUB", data, opcode, inst)
return decodeAddressRegisterOp("SUB", data, opcode, inst)
}
return decodeDirectedBinaryOp("SUB", data, opcode, inst)
}
Expand Down Expand Up @@ -60,25 +60,3 @@ func isAddressRegisterArithmetic(opcode uint16) bool {
opmode := (opcode >> 6) & 0x7
return opmode == 3 || opmode == 7
}

func decodeAddressRegisterArithmetic(mnemonic string, data []byte, opcode uint16, inst *Instruction) error {
opmode := (opcode >> 6) & 0x7
dstReg := uint8((opcode >> 9) & 0x7)
srcMode := uint8((opcode >> 3) & 0x7)
srcReg := uint8(opcode & 0x7)

sizeStr := "W"
sizeBytes := 2
if opmode == 7 {
sizeStr = "L"
sizeBytes = 4
}

srcOperand, offset, srcMeta, err := decodeEAWithSize(data, inst.Address, 2, srcMode, srcReg, sizeBytes)
if err != nil {
return err
}

setInstruction(data, inst, offset, mnemonic+"A."+sizeStr, fmt.Sprintf("%s, A%d", srcOperand, dstReg), srcMeta, registerOperand(RegisterKindAddress, dstReg))
return nil
}
12 changes: 3 additions & 9 deletions internal/decoders/bcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ func decodeBCD(mn string, data []byte, opcode uint16, inst *Instruction) error {
}
srcText := fmt.Sprintf("-(A%d)", srcReg)
dstText := fmt.Sprintf("-(A%d)", dstReg)
setInstruction(data, inst, 2, mn, fmt.Sprintf("%s, %s", srcText, dstText), effectiveAddressOperand(srcText, EffectiveAddress{
Kind: EAKindPreDecrement,
Base: &Register{Kind: RegisterKindAddress, Number: srcReg},
Register: srcReg,
}), effectiveAddressOperand(dstText, EffectiveAddress{
Kind: EAKindPreDecrement,
Base: &Register{Kind: RegisterKindAddress, Number: dstReg},
Register: dstReg,
}))
setInstruction(data, inst, 2, mn, fmt.Sprintf("%s, %s", srcText, dstText),
addrIndirectOperand(EAKindPreDecrement, srcReg, srcText),
addrIndirectOperand(EAKindPreDecrement, dstReg, dstText))
return nil
}
Loading
Loading