1
0
mirror of https://github.com/fadden/6502bench.git synced 2026-04-21 10:16:42 +00:00

Fix Merlin 32 DP op generation

Most assemblers treat '<' and '>' as byte-select operators.  Merlin 32
treats them as shift operators, making '<' effectively a no-op.  A
mask is applied based on the length implied by the opcode or pseudo-op,
e.g. "DFB <FOO" will mask off the high bits.  This is problematic for
instructions like "LDA <FOO", where the choice between absolute and DP
addressing depends on the value of "<FOO".  If FOO is >= $100, the
lack of masking will cause it be treated as absolute.  There is
currently no other mechanism to force the use of a direct-page opcode.

The only recourse is to append "&$ff" to the operand, so the
assembler knows that it can be handled as a DP op.  The code generator
has been updated to add "&$ff" where needed, based on the label's
value.  Unfortunately the assembler doesn't accept this for certain
specific addressing modes; this appears to be a bug.

The relevant test cases (2003x-labels-and-symbols) have been updated
to exercise these situations.  The current test projects side-step the
failing assembler behavior by using a DP label instead.  These should
be changed to correctly exercise the full behavior, with the code
generator outputting the instructions as raw hex values to work
around bugs.

There are some deliberately broken things (like a duplicate label) in
the 20030 case that were copied into the 20032 case when it was
created.  For ease of editing these have been removed from 20032.

(issue #170)
This commit is contained in:
Andy McFadden
2025-07-10 11:17:41 -07:00
parent 9d01f9b205
commit 459cde40c4
21 changed files with 726 additions and 196 deletions
+39 -18
View File
@@ -88,6 +88,8 @@ namespace Asm65 {
/// <summary>String to prefix operand with to force DP addressing.</summary>
public string ForceDirectOperandPrefix { get; set; } = string.Empty;
/// <summary>String to suffix operand with to force DP addressing.</summary>
public string ForceDirectOperandSuffix { get; set; } = string.Empty;
/// <summary>String to suffix opcode with to force abs addressing.</summary>
public string ForceAbsOpcodeSuffix { get; set; } = string.Empty;
/// <summary>String to prefix operand with to force abs addressing.</summary>
@@ -172,6 +174,7 @@ namespace Asm65 {
BankSelectBackQuote = src.BankSelectBackQuote;
ForceDirectOperandPrefix = src.ForceDirectOperandPrefix;
ForceDirectOperandSuffix = src.ForceDirectOperandSuffix;
ForceAbsOpcodeSuffix = src.ForceAbsOpcodeSuffix;
ForceAbsOperandPrefix = src.ForceAbsOperandPrefix;
ForceDirectOpcodeSuffix = src.ForceDirectOpcodeSuffix;
@@ -915,19 +918,23 @@ namespace Asm65 {
private string GenerateOperandFormat(OpDef.AddressMode addrMode,
OpDef.WidthDisambiguation wdis) {
string fmt;
string wdisStr = string.Empty;
string pfxStr = string.Empty;
string dpsfxStr = string.Empty;
if (wdis == OpDef.WidthDisambiguation.ForceDirect) {
if (!string.IsNullOrEmpty(mFormatConfig.ForceDirectOperandPrefix)) {
wdisStr = mFormatConfig.ForceDirectOperandPrefix;
pfxStr = mFormatConfig.ForceDirectOperandPrefix;
}
if (!string.IsNullOrEmpty(mFormatConfig.ForceDirectOperandSuffix)) {
dpsfxStr = mFormatConfig.ForceDirectOperandSuffix;
}
} else if (wdis == OpDef.WidthDisambiguation.ForceAbs) {
if (!string.IsNullOrEmpty(mFormatConfig.ForceAbsOperandPrefix)) {
wdisStr = mFormatConfig.ForceAbsOperandPrefix;
pfxStr = mFormatConfig.ForceAbsOperandPrefix;
}
} else if (wdis == OpDef.WidthDisambiguation.ForceLong) {
if (!string.IsNullOrEmpty(mFormatConfig.ForceLongOperandPrefix)) {
wdisStr = mFormatConfig.ForceLongOperandPrefix;
pfxStr = mFormatConfig.ForceLongOperandPrefix;
}
} else if (wdis == OpDef.WidthDisambiguation.ForceLongMaybe) {
// Don't add a width disambiguator to an operand that is unambiguously long.
@@ -940,35 +947,29 @@ namespace Asm65 {
case AddressMode.AbsLong:
case AddressMode.BlockMove:
case AddressMode.StackAbs:
case AddressMode.DP:
case AddressMode.DPPCRel: // BBR/BBS
case AddressMode.DPPCRel: // BBR/BBS (syntax varies)
case AddressMode.PCRel:
case AddressMode.PCRelLong: // BRL
case AddressMode.StackInt: // COP and two-byte BRK
case AddressMode.StackPCRelLong: // PER
case AddressMode.WDM:
fmt = wdisStr + "{0}";
fmt = pfxStr + "{0}";
break;
case AddressMode.AbsIndexX:
case AddressMode.AbsIndexXLong:
case AddressMode.DPIndexX:
fmt = wdisStr + "{0}," + mXregChar;
fmt = pfxStr + "{0}," + mXregChar;
break;
case AddressMode.DPIndexY:
case AddressMode.AbsIndexY:
fmt = wdisStr + "{0}," + mYregChar;
fmt = pfxStr + "{0}," + mYregChar;
break;
case AddressMode.AbsIndexXInd:
case AddressMode.DPIndexXInd:
fmt = wdisStr + "({0}," + mXregChar + ")";
fmt = pfxStr + "({0}," + mXregChar + ")";
break;
case AddressMode.AbsInd:
case AddressMode.DPInd:
case AddressMode.StackDPInd: // PEI
fmt = "({0})";
break;
case AddressMode.AbsIndLong:
case AddressMode.DPIndLong:
// IIgs monitor uses "()" for AbsIndLong, E&L says "[]". Assemblers
// seem to expect the latter.
fmt = "[{0}]";
@@ -976,11 +977,31 @@ namespace Asm65 {
case AddressMode.Acc:
fmt = mAccChar;
break;
case AddressMode.DP:
fmt = pfxStr + "{0}" + dpsfxStr;
break;
case AddressMode.DPIndexX:
fmt = pfxStr + "{0}" + dpsfxStr + "," + mXregChar;
break;
case AddressMode.DPIndexY:
fmt = pfxStr + "{0}" + dpsfxStr + "," + mYregChar;
break;
case AddressMode.DPIndexXInd:
fmt = pfxStr + "({0}" + dpsfxStr + "," + mXregChar + ")";
break;
case AddressMode.DPInd:
fmt = "({0}" + dpsfxStr + ")";
break;
case AddressMode.DPIndLong:
// IIgs monitor uses "()" for AbsIndLong, E&L says "[]". Assemblers
// seem to expect the latter.
fmt = "[{0}" + dpsfxStr + "]";
break;
case AddressMode.DPIndIndexY:
fmt = "({0})," + mYregChar;
fmt = "({0}" + dpsfxStr + ")," + mYregChar;
break;
case AddressMode.DPIndIndexYLong:
fmt = "[{0}]," + mYregChar;
fmt = "[{0}" + dpsfxStr + "]," + mYregChar;
break;
case AddressMode.Imm:
case AddressMode.ImmLongA:
@@ -1021,7 +1042,7 @@ namespace Asm65 {
/// <returns>Formatted string.</returns>
public string FormatOperand(OpDef op, string contents, OpDef.WidthDisambiguation wdis) {
Debug.Assert(((int)op.AddrMode & 0xff) == (int) op.AddrMode);
int key = (int) op.AddrMode | ((int)wdis << 8);
int key = (int) op.AddrMode | ((int)wdis << 8); // form unique cache key
if (!mOperandFormats.TryGetValue(key, out string format)) {
format = mOperandFormats[key] = GenerateOperandFormat(op.AddrMode, wdis);
+1 -1
View File
@@ -124,7 +124,7 @@ namespace Asm65 {
/// </summary>
public enum WidthDisambiguation : byte {
None = 0,
ForceDirect, // only needed for forward DP label refs in single-pass assemblers
ForceDirect, // mainly needed for forward DP label refs in single-pass assemblers
ForceAbs,
ForceLong,
ForceLongMaybe // add opcode suffix but not operand prefix
+5 -2
View File
@@ -178,6 +178,7 @@ namespace SourceGen.AsmGen {
Quirks.NoPcRelBankWrap = true;
Quirks.TracksSepRepNotEmu = true;
Quirks.ByteSelectionIsShift = true;
mWorkDirectory = workDirectory;
mFileNameBase = fileNameBase;
@@ -200,6 +201,7 @@ namespace SourceGen.AsmGen {
config.ForceAbsOpcodeSuffix = ":";
config.ForceLongOpcodeSuffix = "l";
config.ForceDirectOperandPrefix = string.Empty;
config.ForceDirectOperandSuffix = "&$ff";
config.ForceAbsOperandPrefix = string.Empty;
config.ForceLongOperandPrefix = string.Empty;
config.LocalVariableLabelPrefix = "]";
@@ -841,9 +843,10 @@ namespace SourceGen.AsmGen {
}
// Stdout: "C:\Src\WorkBench\Merlin32.exe v 1.0, (c) Brutal Deluxe ..."
// "C:\Src\WorkBench\Merlin32.exe v 1.2 beta 1, (c) Brutal Deluxe ..."
// "... Merlin32.exe v 1.2 beta 1, (c) Brutal Deluxe ..."
// "... Merlin32_119.exe v1.1.9, (c) Brutal Deluxe ..."
// Other platforms may not have the ".exe". Start at first occurrence of " v ".
private static string sVersionPattern = @" v (\d.\d)( [^,]+)?,";
private static string sVersionPattern = @" v *(\d.\d)(.\d)?( [^,]+)?,";
private static Regex sVersionRegex = new Regex(sVersionPattern);
// IAssembler
+16 -3
View File
@@ -240,12 +240,13 @@ namespace SourceGen.AsmGen {
if (op.IsWidthPotentiallyAmbiguous) {
wdis = OpDef.GetWidthDisambiguation(instrLen, operand);
}
// Some DP addressing modes are ambiguous to one-pass assemblers.
if (gen.Quirks.SinglePassAssembler && wdis == OpDef.WidthDisambiguation.None &&
(op.AddrMode == OpDef.AddressMode.DP ||
op.AddrMode == OpDef.AddressMode.DPIndexX) ||
op.AddrMode == OpDef.AddressMode.DPIndexY) {
op.AddrMode == OpDef.AddressMode.DPIndexX ||
op.AddrMode == OpDef.AddressMode.DPIndexY)) {
// Could be a forward reference to a direct-page label. For ACME, we don't
// care if it's forward or not.
// care if it's forward or not, only that it's referencing a user label.
if ((gen.Quirks.SinglePassNoLabelCorrection && IsLabelReference(gen, offset)) ||
IsForwardLabelReference(gen, offset)) {
wdis = OpDef.WidthDisambiguation.ForceDirect;
@@ -343,6 +344,18 @@ namespace SourceGen.AsmGen {
formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable,
lvLookup, gen.Localizer.LabelMap, dfd,
offset, operandForSymbol, operandLen, opFlags);
// Handle special case for DP args with non-DP labels.
if (gen.Quirks.ByteSelectionIsShift && wdis == OpDef.WidthDisambiguation.None &&
op.IsDirectPageInstruction && dfd.SymbolRef != null) {
// The '<' operator is effectively a no-op, so "LDA <LABEL" won't be a DP
// instruction unless LABEL is < $100. We need to add an explicit mask in
// that case.
Debug.Assert(operand < 0x100); // actual operand in code is DP
if (proj.SymbolTable.TryGetNonVariableValue(dfd.SymbolRef.Label,
out Symbol sym) && sym.Value > 0xff) {
wdis = OpDef.WidthDisambiguation.ForceDirect;
}
}
}
} else {
// Show operand value in hex.
+6
View File
@@ -280,6 +280,12 @@ namespace SourceGen.AsmGen {
/// track the emulation bit?
/// </summary>
public bool TracksSepRepNotEmu { get; set; }
/// <summary>
/// Do the byte selection operators ('&lt;', '&gt;', '^') act as shifts that don't
/// reduce the value to 8 bits?
/// </summary>
public bool ByteSelectionIsShift { get; set; }
}
/// <summary>
Binary file not shown.
@@ -1,8 +1,8 @@
### 6502bench SourceGen dis65 v1.0 ###
{
"_ContentVersion":3,
"FileDataLength":452,
"FileDataCrc32":-1349239236,
"FileDataLength":460,
"FileDataCrc32":-736313654,
"ProjectProps":{
"CpuName":"6502",
"IncludeUndocumentedInstr":false,
@@ -602,7 +602,37 @@
"SubFormat":"Symbol",
"SymbolRef":{
"Label":"BMI",
"Part":"Low"}}},
"Part":"Low"}},
"451":{
"Length":2,
"Format":"NumericLE",
"SubFormat":"Symbol",
"SymbolRef":{
"Label":"targ",
"Part":"Low"}},
"453":{
"Length":2,
"Format":"NumericLE",
"SubFormat":"Symbol",
"SymbolRef":{
"Label":"targ",
"Part":"Low"}},
"455":{
"Length":2,
"Format":"NumericLE",
"SubFormat":"Symbol",
"SymbolRef":{
"Label":"targ",
"Part":"Low"}},
"457":{
"Length":2,
"Format":"NumericLE",
"SubFormat":"Symbol",
"SymbolRef":{
"Label":"targ",
"Part":"Low"}}
},
"LvTables":{
},
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -109,6 +109,10 @@ calls jsr L1015
.text "he quick brown fox jumps over the lazy dogs."
L1160 adc #BMI1
lda <targ-1
lda <targ+1
lda <targ-1,x
lda (<targ+4),y
rts
.here
@@ -104,6 +104,10 @@ calls jsr L1015
!text "he quick brown fox jumps over the lazy dogs."
L1160 adc #BMI1
lda+1 <targ-1
lda+1 <targ+1
lda+1 <targ-1,x
lda (<targ+4),y
rts
}
@@ -107,5 +107,9 @@ calls: jsr L1015
.byte "he quick brown fox jumps over the lazy dogs."
L1160: adc #BMI1
lda <targ-1
lda <targ+1
lda <targ-1,x
lda (<targ+4),y
rts
@@ -103,5 +103,9 @@ calls jsr L1015
asc 'he quick brown fox jumps over the lazy dogs.'
L1160 adc #BMI
lda <targ-1&$ff
lda <targ+1&$ff
lda <targ-1&$ff,x
lda (<targ+4&$ff),y
rts
@@ -170,9 +170,9 @@ _L103D jmp _targ+1
_L1040 jml _targ-1
jml _targ
_L1044 jml _targ
jml _targ+1
_L1048 jml _targ+1
_calls jsr _L1016
jsr _L1028
@@ -185,8 +185,8 @@ _calls jsr _L1016
jsr _L103A
jsr _L103D
jsr _L1040
jsr $1044
jsr $1048
jsr _L1044
jsr _L1048
brl _L118E
_bulk .byte $80,$81,$82,$83,$84,$85,$86,$87,$88,$89,$8a,$8b,$8c,$8d,$8e,$8f ;bulky
@@ -209,6 +209,23 @@ _L118E lda #<thirty2+2
lda #(thirty2 & $ffff)+3
lda #((thirty2 >> 8) & $ffff)+4
lda #thirty2 >> 16
before nop
lda before
lda <before
lda <before+1
lda (<before+2)
lda (<before+3),y
lda [<before+4],y
lda [<before+5]
lda <before+6,x
lda (zip+7,x)
lda before-$10f8,y
lda <after+6
lda (<after+7),y
after ldx <after+8,y
ldy <after+9,x
pei (zip+4)
nop
rts
.here
@@ -22,5 +22,6 @@
!hex 7420746865206c6162656c20616e6420636f6d6d656e74206f6e6c7920617070
!hex 656172206f6e20746865206669727374206c696e652e20205468652071756963
!hex 6b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920
!hex 646f67732ea97aa959a934c230a97b56a95a34a9341260
!hex 646f67732ea97aa959a934c230a97b56a95a34a93412eaad9f11a59fa5a0b2a1
!hex b1a2b7a3a7a4b5a5a1d4b9a700a5c0b1c1b6c2b4c3d4d1ea60
}
@@ -167,9 +167,9 @@ start: clc
@L1040: jml @targ-1
jml @targ
@L1044: jml @targ
jml @targ+1
@L1048: jml @targ+1
@calls: jsr @L1016
jsr @L1028
@@ -182,8 +182,8 @@ start: clc
jsr @L103A
jsr @L103D
jsr @L1040
jsr $1044
jsr $1048
jsr @L1044
jsr @L1048
brl @L118E
@bulk: .byte $80,$81,$82,$83,$84,$85,$86,$87,$88,$89,$8a,$8b,$8c,$8d,$8e,$8f ;bulky
@@ -206,5 +206,22 @@ start: clc
lda #thirty2 & $ffff +3
lda #thirty2 >> 8 & $ffff +4
lda #thirty2 >> 16
before: nop
lda before
lda <before
lda <before+1
lda (<before+2)
lda (<before+3),y
lda [<before+4],y
lda [<before+5]
lda <before+6,x
lda (zip+7,x)
lda before-$10f8,y
lda z:<after+6
lda (<after+7),y
after: ldx <after+8,y
ldy <after+9,x
pei (zip+4)
nop
rts
@@ -162,9 +162,9 @@ start clc
:L1040 jml :targ-1
jml :targ
:L1044 jml :targ
jml :targ+1
:L1048 jml :targ+1
:calls jsr :L1016
jsr :L1028
@@ -177,8 +177,8 @@ start clc
jsr :L103A
jsr :L103D
jsr :L1040
jsr $1044
jsr $1048
jsr :L1044
jsr :L1048
brl :L118E
:bulk hex 808182838485868788898a8b8c8d8e8f808182838485868788898a8b8c8d8e8f ;bulky
@@ -197,5 +197,22 @@ start clc
lda #thirty2+3
lda #>thirty2+$400
lda #^thirty2
before nop
lda before
lda <before&$ff
lda <before+1&$ff
dfb $b2,$a1
lda (<before+3&$ff),y
lda [<before+4&$ff],y
dfb $a7,$a4
lda <before+6&$ff,x
lda (zip+7,x)
lda before-$10f8,y
lda <after+6&$ff
lda (<after+7&$ff),y
after ldx <after+8&$ff,y
ldy <after+9&$ff,x
pei (zip+4)
nop
rts
@@ -119,7 +119,7 @@ L1000 lda CodeWrap+255
lda $4000
lda $4001
lda BankWrap+8
lda <BankWrap-$ffe8
lda <BankWrap-$ffe8&$ff
nop
lda ReadOnly
lda ReadOnly+1
@@ -131,4 +131,8 @@ next
:skiphex
adc #$30 ;set to BMI to test constant vs. instr
lda <target0&$ff
lda <target2&$ff
lda <target0&$ff,x
lda (<target2+3&$ff),y
rts
@@ -228,5 +228,28 @@ t4c jml target2
lda #>thirty2 + 1024
lda #^thirty2
; test direct-page access with a non-DP label, forward and backward, with adjustments
before nop
lda before
lda <before&$ff
lda <before+1&$ff
lda (<before+2&$ff)
lda (<before+3&$ff),y
lda [<before+4&$ff],y
lda [<before+5&$ff]
lda <before+6&$ff,x
lda (<zip+7&$ff,x) ;all versions of Merlin 32 fail
lda <before+8&$ff,y
lda <after&$ff
lda (<after+1&$ff),y
ldx <after+2&$ff,y
ldy <after+3&$ff,x
pei (<zip+4&$ff)
after nop
rts
+6 -1
View File
@@ -330,8 +330,11 @@ target assembler doesn't handle it.</p>
<a href="https://www.brutaldeluxe.fr/products/crossdevtools/merlin/">[web site]</a>
<a href="https://github.com/apple2accumulator/merlin32/issues">[bug tracker]</a>
</p>
<p>The history is somewhat complicated, as there are two different versions
of Merlin v1.1, updated by different authors. These are referred to as
the "official" and "forked" versions.</p>
<p>Bugs:</p>
<p>Bugs (present in v1.0; unclear if/when these have been fixed):</p>
<ul>
<li>PC relative branches don't wrap around at bank boundaries.</li>
<li>For some failures, an exit code of zero is returned.</li>
@@ -357,6 +360,8 @@ target assembler doesn't handle it.</p>
<li>The byte selection operators ('&lt;', '&gt;', '^') are actually
word-selection operators, yielding 16-bit values when wide registers
are enabled on the 65816.</li>
<li>It's not possible to force direct-page addressing with a modifier.
"&amp;$ff" must be added to the operand.</li>
<li>Values loaded into registers are implicitly mod 256 or 65536. There
is no need to explicitly mask an expression.</li>
<li>The assembler tracks register widths when it sees SEP/REP instructions,