From bd11aea4a49ae69dd610c8a474e015c4d27589a8 Mon Sep 17 00:00:00 2001
From: Andy McFadden <fadden@fadden.com>
Date: Wed, 16 Oct 2019 17:32:30 -0700
Subject: [PATCH] External symbol I/O direction and address mask, part 3 (of 3)

Added regression tests.  Improved error messages.  Updated
documentation.
---
 SourceGen/PlatformSymbols.cs                  |  27 +++++++-
 SourceGen/Res/Strings.xaml                    |   9 ++-
 SourceGen/Res/Strings.xaml.cs                 |  14 +++-
 SourceGen/RuntimeData/Help/advanced.html      |  62 +++++++++++++++---
 SourceGen/RuntimeData/Help/editors.html       |  25 +++----
 SourceGen/RuntimeData/Help/intro.html         |  15 +++++
 SourceGen/SGTestData/2021-external-symbols    | Bin 222 -> 329 bytes
 .../SGTestData/2021-external-symbols-1.sym65  |  50 ++++++++++++++
 .../SGTestData/2021-external-symbols-2.sym65  |  28 ++++++++
 .../SGTestData/2021-external-symbols.dis65    |   2 +-
 .../Expected/2021-external-symbols_64tass.S   |  59 +++++++++++++++--
 .../Expected/2021-external-symbols_Merlin32.S |  59 +++++++++++++++--
 .../Expected/2021-external-symbols_acme.S     |  59 +++++++++++++++--
 .../Expected/2021-external-symbols_cc65.S     |  59 +++++++++++++++--
 .../Expected/2021-external-symbols_cc65.cfg   |   2 +-
 .../SGTestData/Source/2021-external-symbols.S |  49 ++++++++++++++
 16 files changed, 473 insertions(+), 46 deletions(-)

diff --git a/SourceGen/PlatformSymbols.cs b/SourceGen/PlatformSymbols.cs
index ef37b4d..63a593d 100644
--- a/SourceGen/PlatformSymbols.cs
+++ b/SourceGen/PlatformSymbols.cs
@@ -144,9 +144,9 @@ namespace SourceGen {
                     if (line.StartsWith(TAG_CMD)) {
                         tag = ParseTag(line);
                     } else if (line.StartsWith(MULTI_MASK_CMD)) {
-                        if (!ParseMask(line, out multiMask)) {
+                        if (!ParseMask(line, out multiMask, out string badMaskMsg)) {
                             report.Add(lineNum, FileLoadItem.NO_COLUMN, FileLoadItem.Type.Warning,
-                                Res.Strings.ERR_INVALID_MASK);
+                                badMaskMsg);
                         }
                         //Debug.WriteLine("Mask is now " + mask.ToString("x6"));
                     } else {
@@ -268,12 +268,15 @@ namespace SourceGen {
         /// <param name="line">Line to parse.</param>
         /// <param name="multiMask">Parsed mask value, or null if the line was empty.</param>
         /// <returns>True if the mask was parsed successfully.</returns>
-        private bool ParseMask(string line, out DefSymbol.MultiAddressMask multiMask) {
+        private bool ParseMask(string line, out DefSymbol.MultiAddressMask multiMask,
+                out string badMaskMsg) {
             Debug.Assert(line.StartsWith(MULTI_MASK_CMD));
             const int MIN = 0;
             const int MAX = 0x00ffff;
 
+            badMaskMsg = Res.Strings.ERR_INVALID_MULTI_MASK;
             multiMask = null;
+
             string maskStr = line.Substring(MULTI_MASK_CMD.Length).Trim();
             if (string.IsNullOrEmpty(maskStr)) {
                 // empty line, disable mask
@@ -293,16 +296,34 @@ namespace SourceGen {
             if (!Asm65.Number.TryParseInt(cmpMaskStr, out cmpMask, out ignoredBase) ||
                     cmpMask < MIN || cmpMask > MAX) {
                 Debug.WriteLine("Bad cmpMask: " + cmpMaskStr);
+                badMaskMsg = Res.Strings.ERR_INVALID_COMPARE_MASK;
                 return false;
             }
             if (!Asm65.Number.TryParseInt(cmpValueStr, out cmpValue, out ignoredBase) ||
                     cmpValue < MIN || cmpValue > MAX) {
                 Debug.WriteLine("Bad cmpValue: " + cmpValueStr);
+                badMaskMsg = Res.Strings.ERR_INVALID_COMPARE_VALUE;
                 return false;
             }
             if (!Asm65.Number.TryParseInt(addrMaskStr, out addrMask, out ignoredBase) ||
                     addrMask < MIN || addrMask > MAX) {
                 Debug.WriteLine("Bad addrMask: " + addrMaskStr);
+                badMaskMsg = Res.Strings.ERR_INVALID_ADDRESS_MASK;
+                return false;
+            }
+
+            // The two masks should not overlap: one represents bits that must be in a
+            // specific state for a match to exist, the other indicates which bits are used
+            // to select a specific register.  This should be a warning.
+            if ((cmpMask & ~addrMask) != cmpMask) {
+                Debug.WriteLine("Warning: cmpMask/addrMask overlap");
+                badMaskMsg = Res.Strings.ERR_INVALID_CMP_ADDR_OVERLAP;
+                return false;
+            }
+            // If cmpValue has bits set that aren't in cmpMask, we will never find a match.
+            if ((cmpValue & ~cmpMask) != 0) {
+                Debug.WriteLine("cmpValue has unexpected bits set");
+                badMaskMsg = Res.Strings.ERR_INVALID_CMP_EXTRA_BITS;
                 return false;
             }
 
diff --git a/SourceGen/Res/Strings.xaml b/SourceGen/Res/Strings.xaml
index 6c04dfa..4edefc6 100644
--- a/SourceGen/Res/Strings.xaml
+++ b/SourceGen/Res/Strings.xaml
@@ -61,9 +61,14 @@ limitations under the License.
     <system:String x:Key="str_ErrFileNotFoundFmt">File not found: {0}</system:String>
     <system:String x:Key="str_ErrFileReadFailedFmt">Failed reading {0}: {1}.</system:String>
     <system:String x:Key="str_ErrFileReadOnlyFmt">Cannot write to read-only file {0}.</system:String>
+    <system:String x:Key="str_ErrInvalidAddressMask">Invalid MULTI_MASK AddressMask</system:String>
+    <system:String x:Key="str_ErrInvalidCmpAddrOverlap">MULTI_MASK CompareMask and AddressMask overlap</system:String>
+    <system:String x:Key="str_ErrInvalidCmpExtraBits">MULTI_MASK CompareValue has bits not in CompareMask</system:String>
+    <system:String x:Key="str_ErrInvalidCompareMask">Invalid MULTI_MASK CompareMask</system:String>
+    <system:String x:Key="str_ErrInvalidCompareValue">Invalid MULTI_MASK CompareValue</system:String>
     <system:String x:Key="str_ErrInvalidIntValue">Could not convert value to integer</system:String>
     <system:String x:Key="str_ErrInvalidKeyValue">Key value is out of range</system:String>
-    <system:String x:Key="str_ErrInvalidMask">Invalid mask value</system:String>
+    <system:String x:Key="str_ErrInvalidMultiMask">Invalid MULTI_MASK line</system:String>
     <system:String x:Key="str_ErrInvalidWidth">Invalid width value</system:String>
     <system:String x:Key="str_ErrInvalidSysdef" xml:space="preserve"> - INVALID DEFINITION</system:String>
     <system:String x:Key="str_ErrLoadConfigFile">Unable to load config file</system:String>
@@ -72,7 +77,7 @@ limitations under the License.
     <system:String x:Key="str_ErrProjectLoadFail">Unable to load project file</system:String>
     <system:String x:Key="str_ErrProjectSaveFail">Unable to save project file</system:String>
     <system:String x:Key="str_ErrTooLargeForPreview">[File was too large for preview window]</system:String>
-    <system:String x:Key="str_ErrValueIncompatibleWithMask">Symbol value is incompatible with multi-mask</system:String>
+    <system:String x:Key="str_ErrValueIncompatibleWithMask">Symbol value is incompatible with current multi-mask</system:String>
     <system:String x:Key="str_ExternalFileBadDirFmt" xml:space="preserve">Symbol files and extension scripts must live in the application runtime directory ({0}) or project directory ({1}).&#x0d;&#x0d;File {2} lives elsewhere.</system:String>
     <system:String x:Key="str_ExternalFileBadDirCaption">File Not In Runtime Directory</system:String>
     <system:String x:Key="str_FileFilterAll">All files (*.*)|*.*</system:String>
diff --git a/SourceGen/Res/Strings.xaml.cs b/SourceGen/Res/Strings.xaml.cs
index c667cdc..6461abd 100644
--- a/SourceGen/Res/Strings.xaml.cs
+++ b/SourceGen/Res/Strings.xaml.cs
@@ -103,12 +103,22 @@ namespace SourceGen.Res {
             (string)Application.Current.FindResource("str_ErrFileReadFailedFmt");
         public static string ERR_FILE_READ_ONLY_FMT =
             (string)Application.Current.FindResource("str_ErrFileReadOnlyFmt");
+        public static string ERR_INVALID_ADDRESS_MASK =
+            (string)Application.Current.FindResource("str_ErrInvalidAddressMask");
+        public static string ERR_INVALID_CMP_ADDR_OVERLAP =
+            (string)Application.Current.FindResource("str_ErrInvalidCmpAddrOverlap");
+        public static string ERR_INVALID_CMP_EXTRA_BITS =
+            (string)Application.Current.FindResource("str_ErrInvalidCmpExtraBits");
+        public static string ERR_INVALID_COMPARE_MASK =
+            (string)Application.Current.FindResource("str_ErrInvalidCompareMask");
+        public static string ERR_INVALID_COMPARE_VALUE =
+            (string)Application.Current.FindResource("str_ErrInvalidCompareValue");
         public static string ERR_INVALID_INT_VALUE =
             (string)Application.Current.FindResource("str_ErrInvalidIntValue");
         public static string ERR_INVALID_KEY_VALUE =
             (string)Application.Current.FindResource("str_ErrInvalidKeyValue");
-        public static string ERR_INVALID_MASK =
-            (string)Application.Current.FindResource("str_ErrInvalidMask");
+        public static string ERR_INVALID_MULTI_MASK =
+            (string)Application.Current.FindResource("str_ErrInvalidMultiMask");
         public static string ERR_INVALID_WIDTH =
             (string)Application.Current.FindResource("str_ErrInvalidWidth");
         public static string ERR_INVALID_SYSDEF =
diff --git a/SourceGen/RuntimeData/Help/advanced.html b/SourceGen/RuntimeData/Help/advanced.html
index e1f9c3a..1a92ed5 100644
--- a/SourceGen/RuntimeData/Help/advanced.html
+++ b/SourceGen/RuntimeData/Help/advanced.html
@@ -27,12 +27,14 @@ matters.</p>
 
 <p>Platform symbol files consist of comments, commands, and symbols.
 Blank lines, and lines that begin with a semicolon (';'), are ignored.  Lines
-that begin with an asterisk ('*') are commands.  Two are currently
+that begin with an asterisk ('*') are commands.  Three are currently
 defined:</p>
 <ul>
   <li><code>*SYNOPSIS</code> - a short summary of the file contents.</li>
   <li><code>*TAG</code> - a tag string to apply to all symbols that follow
     in this file.</li>
+  <li><code>*MULTI_MASK</code> - specify a mask for symbols that appear
+    at multiple addresses.</li>
 </ul>
 
 <p>Tags can be used by extension scripts to identify a subset of symbols.
@@ -44,30 +46,70 @@ are treated as untagged.</p>
 
 <p>All other lines are symbols, which have the form:</p>
 <pre>
-  label {=|@} value [width] [;comment]
+  LABEL {=|@|&lt;|&gt;} VALUE [WIDTH] [;COMMENT]
 </pre>
 
-<p>Labels must be at least two characters long, begin with a letter or
+<p>The LABEL must be at least two characters long, begin with a letter or
 underscore, and consist entirely of alphanumeric ASCII characters
 (A-Z, a-z, 0-9) and the underscore ('_').  (This is the same format
 required for line labels in SourceGen.)</p>
-<p>Use '@' for address values, and '=' for constants.  The only important
-difference between them is that address values will be applied automatically
-to operands that reference addresses outside the scope of the file.
-Constants are never applied automatically.</p>
+<p>The next token can be one of:</p>
+<ul>
+  <li>@: general addresses</li>
+  <li>&lt;: read-only addresses</li>
+  <li>&gt;: write-only addresses</li>
+  <li>=: constants</li>
+</ul>
+<p>If an instruction references an address, and that address is outside
+the bounds of the file, the list of address symbols (i.e. everything
+that's not a constant) will be scanned for a match.
+If found, the symbol is applied automatically.  You normally want to
+use '@', but can use '&lt;' and '&gt;' for memory-mapped I/O locations
+that have different behavior depending on whether they are read or
+written.</p>
 
-<p>The value is a number in decimal, hexadecimal (with a leading '$'), or
+<p>The VALUE is a number in decimal, hexadecimal (with a leading '$'), or
 binary (with a leading '%').  The numeric base will be recorded and used when
 formatting the symbol in generated output, so use whichever form is most
 appropriate.  Values are unsigned 24-bit numbers.</p>
 
-<p>The width is optional, and ignored for constants.  It must be a
+<p>The WIDTH is optional, and ignored for constants.  It must be a
 decimal or hexadecimal value between 1 and 65536, inclusive.  If omitted,
 the default width is 1.</p>
 
-<p>The comment is optional.  If present, it will be saved and used as the
+<p>The COMMENT is optional.  If present, it will be saved and used as the
 end-of-line comment on the .EQ directive if the symbol is used.</p>
 
+<h4>Using MULTI_MASK</h4>
+
+<p>The multi-address mask is used for systems like the Atari 2600, where
+RAM, ROM, and I/O registers appear at multiple addresses.  The hardware
+looks for certain address lines to be set or clear, and if the pattern
+matches, another set of bits is examined to determine which register or
+RAM address is being accessed.</p>
+
+<p>For example, suppose the access pattern for a set of registers is
+<code>???0 ??1? 1??x xxxx</code> (where '?' can be any value, 0/1 must
+be that value, and 'x' means the bit is used to determine the register).
+So any value between $0280-029f matches, as does $23c0-23df, but
+$0480 and $1280 don't.  The register number is found in the low five bits.</p>
+<p>The corresponding MULTI_MASK line, with values specifed in binary,
+would be:</p>
+<pre>*MULTI_MASK %0001001010000000 %0000001010000000 %0000000000011111</pre>
+<p>The values are CompareMask, CompareValue, and AddressMask.  To
+determine if an address is in the register set, we check to see if
+<code>(address &amp; CompareMask) == CompareValue</code>.  If so, we can
+extract the register number with <code>(address &amp; AddressMask)</code>.</p>
+
+<p>We don't want to have a huge collection of equates at the top of the
+file, so whatever value is used in the symbol declaration is considered
+the "canonical" value.  All other matching values are expressed as an
+offset.</p>
+<p>All values must fall between 0 and $00FFFFFF.  The set bits in
+CompareMask and AddressMask must not overlap, and CompareValue must not
+have any bits set that aren't also set in CompareMask.</p>
+
+
 <h3>Creating a Project-Specific Symbol File</h3>
 
 <p>To create a platform symbol file for your project, just create a new
diff --git a/SourceGen/RuntimeData/Help/editors.html b/SourceGen/RuntimeData/Help/editors.html
index b56ff2b..395044b 100644
--- a/SourceGen/RuntimeData/Help/editors.html
+++ b/SourceGen/RuntimeData/Help/editors.html
@@ -295,6 +295,11 @@ to the note in the code list and in the "Notes" window.</p>
 
 <h2><a name="project-symbol">Edit Project Symbol</a></h2>
 <p>This is used to edit the properties of a project symbol.</p>
+<p>Symbols marked as "address" will be applied automatically when an
+operand references an address outside the scope of the data file.  They
+will not be applied to addresses inside the data file.  Symbols
+marked as "constant" are not applied automatically, and must be
+explicitly specified as an operand.</p>
 <p>The label must meet the criteria for symbols (see
 <a href="intro.html#about-symbols">All About Symbols</a>), and must
 not have the same name as another project symbol.  It can overlap
@@ -302,19 +307,17 @@ with platform symbols and user labels.</p>
 <p>The value may be entered in decimal, hexadecimal, or binary.  The numeric
 base you choose will be remembered, so that the value will be displayed
 the same way when used in a .EQ directive.</p>
-<p>You can optionally provide a width.  For example, if the address is
-of a two-byte pointer or a 64-byte buffer, you would set the width field
-to cause all references to any location in that range to be set to the
-symbol.  Widths may be entered in hex or decimal.  If the field
-is left blank, a width of 1 is assumed.  Overlapping symbols are allowed.
-The width is ignored for constants.</p>
+<p>You can optionally provide a width for address symbols.  For example,
+if the address is of a two-byte pointer or a 64-byte buffer, you would
+set the width field to cause all references to any location in that range
+to be set to the symbol.  Widths may be entered in hex or decimal.  If
+the field is left blank, a width of 1 is assumed.  Overlapping symbols
+are allowed.  The width is ignored for constants.</p>
 <p>If you enter a comment, it will be placed at the end of the line of
 the .EQ directive.</p>
-<p>Symbols marked as "address" will be applied automatically when an
-operand references an address outside the scope of the data file.  They
-will not be applied to addresses inside the data file.  Symbols
-marked as "constant" are not applied automatically, and must be
-explicitly specified as an operand.</p>
+<p>For address symbols that represent a memory-mapped I/O location, it
+can be useful to have different symbols for reads and writes.  Use
+the Read/Write checkboxes to specify the desired behavior.</p>
 
 
 <h2><a name="lvtable">Create/Edit Local Variable Table</a></h2>
diff --git a/SourceGen/RuntimeData/Help/intro.html b/SourceGen/RuntimeData/Help/intro.html
index d4cd6ca..cd5eb1e 100644
--- a/SourceGen/RuntimeData/Help/intro.html
+++ b/SourceGen/RuntimeData/Help/intro.html
@@ -144,6 +144,15 @@ instructions.  If you don't know what state the flags are in, you can't
 know whether <code>LDA #value</code> is two bytes or three, and the
 disassembly of the instruction stream will come out wrong.</p>
 
+<p>Some addresses correspond to memory-mapped I/O, rather than RAM or ROM.
+Accessing the address can have side effects, like changing between text
+and graphics modes.  Sometimes reading and writing have different effects.
+For example, on later models of the Apple II, reading from
+$C000 returns the most recently hit key, while writing to $C000 disables
+80 columns.</p>
+<p>On a few systems, such as the Atari 2600, RAM, ROM, and registers can
+appear at multiple locations, "mirrored" across the address space.</p>
+
 <h3><a name="charenc">Character Encoding</a></h3>
 
 <p>The American Standard Code for Information Interchange (ASCII) was
@@ -459,6 +468,12 @@ a 4-byte symbol in the middle of a 256-byte symbol, the 4-byte symbol will
 be visible because the start point is closer to the addresses it covers
 than the start of the 256-byte range.</p>
 
+<p>Platform symbols can be designated for reading, writing, or both.
+Normally you'd want both, but if an address is a memory-mapped I/O
+location that has different behavior for reads and writes, you'd want
+to define two different symbols, and have the correct one applied
+based on the access type.</p>
+
 <p><b>Project symbols</b> behave like platform symbols, but they are
 defined in the project file itself.  The editor will prevent you from
 creating two symbols with the same name.  If two symbols have the same
diff --git a/SourceGen/SGTestData/2021-external-symbols b/SourceGen/SGTestData/2021-external-symbols
index 0f01d0cd503370fe66b4f23f8ef3a699152bfafd..b1d9202bba3b99a8fab52d6edc7f10ddcd06ea43 100644
GIT binary patch
delta 135
zcmWN{y%EAN3<gmD#DNam6Bud@0-Z)+07gOFu}72{K9@OmU;tIw0NlOeef<`y{!XjQ
zi_dy3dMnE1T|W;~$Le_vyhIkq;`tT$8QI8zGN^%e7<o_uoreObf-W$P&<UELGxPwN
WAPF)<hL5n8Ms}TYr`BojcK-v}T{m9<

delta 28
kcmX@fbdOPJ?SKAt3<7Hy1=ijbSbI-k?Nfn?LZ=xM0H}NmnE(I)

diff --git a/SourceGen/SGTestData/2021-external-symbols-1.sym65 b/SourceGen/SGTestData/2021-external-symbols-1.sym65
index f37a170..c11e471 100644
--- a/SourceGen/SGTestData/2021-external-symbols-1.sym65
+++ b/SourceGen/SGTestData/2021-external-symbols-1.sym65
@@ -53,3 +53,53 @@ BankWrap    @   $fff0   $20
 
 ; Width specifiers on constants should be ignored.
 FatConst    =   $4000   8
+
+; I/O direction test
+ReadOnly    <   $5000 2         ;R
+WriteOnly   >   $5001 2         ;W
+
+
+;
+; MULTI_MASK tests.
+;
+; The behavior of overlapping masks is not currently defined, so we don't test
+; that scenario.
+;
+
+; overlaps with multi range in second symbol file
+AlsoMoreMultiZero @ $c110       ;winner
+
+*MULTI_MASK $ff00 $c000 $000f   ;$c000-c00f, repeats $c010-c01f, etc. to $c0ff
+MultiZero       @   $c000
+AlsoMultiZero   @   $c010       ;wins (alphabetically)
+MultiOne        @   $c021
+; Test: C000, C010, C020, C0F0
+; Test: C001, C011, C021
+; Test: C002, C012, C022
+
+MultiRead       <   $c004 3     ;$c004/5/6, read-only
+MultiWrite      >   $c005 3     ;$c005/6/7, write-only
+; Test: read C003 C004 C005 C006 C007
+; Test: write C004 C005 C006 C007 C008
+
+
+;
+; Invalid values.  These cause a warning at load time, and the symbol will
+; be ignored.
+;
+
+; Not in range.
+MultiInvalid    @   $1234
+
+; Not all covered addresses are inside the masked range.
+TooLong         @   $c0f8   $a
+
+;
+; Badly-formed MULTI_MASK entries.  These cause a warning at load time, and
+; the directive will be ignored.
+;
+*MULTI_MASK $fffff $ffff $ffff  ;range
+*MULTI_MASK $ffff $fffff $ffff  ;range
+*MULTI_MASK $ffff $ffff $fffff  ;range
+
+*MULTI_MASK
diff --git a/SourceGen/SGTestData/2021-external-symbols-2.sym65 b/SourceGen/SGTestData/2021-external-symbols-2.sym65
index b9407f5..388ef04 100644
--- a/SourceGen/SGTestData/2021-external-symbols-2.sym65
+++ b/SourceGen/SGTestData/2021-external-symbols-2.sym65
@@ -11,3 +11,31 @@ SameValB_A  @   $2110
 SameValC_B  @   $2120
 
 SepOver1    @   $3100   4       ;$3100-3103, inclusive
+
+; I/O direction test -- replace part of the write-only range
+WriteOnly2  >   $5002
+
+;
+; MULTI_MASK tests.
+;
+
+; This overlaps with an earlier declaration, but *only* for address $c010,
+; not for all occurrences.
+NonMultiOver    @   $c010       ;winner
+
+
+*MULTI_MASK $ff00 $c100 $000f   ;$c100-c10f, repeats $c110-c11f, etc. to $c1ff
+
+; Symbol in previous file overlaps with this.
+; Test: C100, C110, C120
+MoreMultiZero   @   $c100
+
+
+;
+; More erroneous masks.  These are in a separate file mostly to test how
+; errors in multiple files are reported.
+;
+*MULTI_MASK $fff0 $000f $000f   ;CompareValue has bits not in CompareMask
+*MULTI_MASK $fff0 $fff0 $00ff   ;AddressMask and CompareMask overlap
+
+*MULTI_MASK
diff --git a/SourceGen/SGTestData/2021-external-symbols.dis65 b/SourceGen/SGTestData/2021-external-symbols.dis65
index 00a6715..f5965fa 100644
--- a/SourceGen/SGTestData/2021-external-symbols.dis65
+++ b/SourceGen/SGTestData/2021-external-symbols.dis65
@@ -1,6 +1,6 @@
 ### 6502bench SourceGen dis65 v1.0 ###
 {
-"_ContentVersion":2,"FileDataLength":222,"FileDataCrc32":-233099313,"ProjectProps":{
+"_ContentVersion":2,"FileDataLength":329,"FileDataCrc32":-573118187,"ProjectProps":{
 "CpuName":"6502","IncludeUndocumentedInstr":false,"EntryFlags":32702671,"AutoLabelStyle":"Simple","AnalysisParams":{
 "AnalyzeUncategorizedData":true,"DefaultTextScanMode":"LowHighAscii","MinCharsForString":4,"SeekNearbyTargets":true,"SmartPlpHandling":true},
 "PlatformSymbolFileIdentifiers":["PROJ:2021-external-symbols-1.sym65","PROJ:2021-external-symbols-2.sym65","PROJ:2021-external-symbols-3.sym65"],"ExtensionScriptFileIdentifiers":[],"ProjectSyms":{
diff --git a/SourceGen/SGTestData/Expected/2021-external-symbols_64tass.S b/SourceGen/SGTestData/Expected/2021-external-symbols_64tass.S
index f33b9fe..6832803 100644
--- a/SourceGen/SGTestData/Expected/2021-external-symbols_64tass.S
+++ b/SourceGen/SGTestData/Expected/2021-external-symbols_64tass.S
@@ -18,15 +18,25 @@ Over2a  =       $3006      ;$3006
 Over3   =       $3006      ;$3006-300c
 SepOver1 =      $3100      ;$3100-3103, inclusive
 SepOver2 =      $3102      ;$3102-3105, inclusive
+ReadOnly =      $5000      ;R
+WriteOnly =     $5001      ;W
+WriteOnly2 =    $5002
+MultiRead =     $c004      ;$c004/5/6, read-only
+MultiWrite =    $c005      ;$c005/6/7, write-only
+AlsoMultiZero = $c010      ;wins (alphabetically)
+NonMultiOver =  $c010      ;winner
+MultiOne =      $c021
+MoreMultiZero = $c100
+AlsoMoreMultiZero = $c110  ;winner
 BankWrap =      $fff0
 
 *       =       $1000
 L1000   lda     CodeWrap+255
         ldx     L1000
         ldy     L1000+1
-        lda     L10DD
-        lda     CodeWrap+478
-        lda     CodeWrap+485
+        lda     L1148
+        lda     CodeWrap+585
+        lda     CodeWrap+592
         nop
         lda     $1ffe
         lda     SameName1-1
@@ -103,5 +113,46 @@ LocalVar .var   $41
         lda     $4001
         lda     BankWrap+8
         lda     <BankWrap-232
-L10DD   rts
+        nop
+        lda     ReadOnly
+        lda     ReadOnly+1
+        ldx     $5002
+        ldy     $5003
+        sta     WriteOnly-1
+        sta     WriteOnly
+        stx     WriteOnly2
+        sty     $5003
+        nop
+        bit     AlsoMultiZero-16
+        bit     NonMultiOver
+        bit     AlsoMultiZero+16
+        bit     AlsoMultiZero+224
+        nop
+        bit     MultiOne-32
+        bit     MultiOne-16
+        bit     MultiOne
+        nop
+        bit     $c002
+        bit     $c012
+        bit     $c022
+        nop
+        lda     MultiRead-1
+        lda     MultiRead
+        lda     MultiRead+1
+        lda     MultiRead+2
+        lda     $c007
+        sta     MultiWrite-1
+        sta     MultiWrite
+        sta     MultiWrite+1
+        sta     MultiWrite+2
+        sta     $c008
+        nop
+        jsr     MultiRead+1
+        nop
+        bit     MoreMultiZero
+        bit     AlsoMoreMultiZero
+        bit     MoreMultiZero+32
+        bit     MoreMultiZero+240
+        nop
+L1148   rts
 
diff --git a/SourceGen/SGTestData/Expected/2021-external-symbols_Merlin32.S b/SourceGen/SGTestData/Expected/2021-external-symbols_Merlin32.S
index 186279d..d5b63e8 100644
--- a/SourceGen/SGTestData/Expected/2021-external-symbols_Merlin32.S
+++ b/SourceGen/SGTestData/Expected/2021-external-symbols_Merlin32.S
@@ -17,15 +17,25 @@ Over2a   equ   $3006      ;$3006
 Over3    equ   $3006      ;$3006-300c
 SepOver1 equ   $3100      ;$3100-3103, inclusive
 SepOver2 equ   $3102      ;$3102-3105, inclusive
+ReadOnly equ   $5000      ;R
+WriteOnly equ  $5001      ;W
+WriteOnly2 equ $5002
+MultiRead equ  $c004      ;$c004/5/6, read-only
+MultiWrite equ $c005      ;$c005/6/7, write-only
+AlsoMultiZero equ $c010   ;wins (alphabetically)
+NonMultiOver equ $c010    ;winner
+MultiOne equ   $c021
+MoreMultiZero equ $c100
+AlsoMoreMultiZero equ $c110 ;winner
 BankWrap equ   $fff0
 
          org   $1000
 L1000    lda   CodeWrap+255
          ldx   L1000
          ldy   L1000+1
-         lda   L10DD
-         lda   CodeWrap+478
-         lda   CodeWrap+485
+         lda   L1148
+         lda   CodeWrap+585
+         lda   CodeWrap+592
          nop
          lda   $1ffe
          lda   SameName1-1
@@ -102,5 +112,46 @@ L1000    lda   CodeWrap+255
          lda   $4001
          lda   BankWrap+8
          lda   <BankWrap-65512
-L10DD    rts
+         nop
+         lda   ReadOnly
+         lda   ReadOnly+1
+         ldx   $5002
+         ldy   $5003
+         sta   WriteOnly-1
+         sta   WriteOnly
+         stx   WriteOnly2
+         sty   $5003
+         nop
+         bit   AlsoMultiZero-16
+         bit   NonMultiOver
+         bit   AlsoMultiZero+16
+         bit   AlsoMultiZero+224
+         nop
+         bit   MultiOne-32
+         bit   MultiOne-16
+         bit   MultiOne
+         nop
+         bit   $c002
+         bit   $c012
+         bit   $c022
+         nop
+         lda   MultiRead-1
+         lda   MultiRead
+         lda   MultiRead+1
+         lda   MultiRead+2
+         lda   $c007
+         sta   MultiWrite-1
+         sta   MultiWrite
+         sta   MultiWrite+1
+         sta   MultiWrite+2
+         sta   $c008
+         nop
+         jsr   MultiRead+1
+         nop
+         bit   MoreMultiZero
+         bit   AlsoMoreMultiZero
+         bit   MoreMultiZero+32
+         bit   MoreMultiZero+240
+         nop
+L1148    rts
 
diff --git a/SourceGen/SGTestData/Expected/2021-external-symbols_acme.S b/SourceGen/SGTestData/Expected/2021-external-symbols_acme.S
index 0fa6d15..75b190f 100644
--- a/SourceGen/SGTestData/Expected/2021-external-symbols_acme.S
+++ b/SourceGen/SGTestData/Expected/2021-external-symbols_acme.S
@@ -18,15 +18,25 @@ Over2a  =       $3006      ;$3006
 Over3   =       $3006      ;$3006-300c
 SepOver1 =      $3100      ;$3100-3103, inclusive
 SepOver2 =      $3102      ;$3102-3105, inclusive
+ReadOnly =      $5000      ;R
+WriteOnly =     $5001      ;W
+WriteOnly2 =    $5002
+MultiRead =     $c004      ;$c004/5/6, read-only
+MultiWrite =    $c005      ;$c005/6/7, write-only
+AlsoMultiZero = $c010      ;wins (alphabetically)
+NonMultiOver =  $c010      ;winner
+MultiOne =      $c021
+MoreMultiZero = $c100
+AlsoMoreMultiZero = $c110  ;winner
 BankWrap =      $fff0
 
 *       =       $1000
 L1000   lda     CodeWrap+255
         ldx     L1000
         ldy     L1000+1
-        lda     L10DD
-        lda     CodeWrap+478
-        lda     CodeWrap+485
+        lda     L1148
+        lda     CodeWrap+585
+        lda     CodeWrap+592
         nop
         lda     $1ffe
         lda     SameName1-1
@@ -104,5 +114,46 @@ L1000   lda     CodeWrap+255
         lda     $4001
         lda     BankWrap+8
         lda     <BankWrap-232
-L10DD   rts
+        nop
+        lda     ReadOnly
+        lda     ReadOnly+1
+        ldx     $5002
+        ldy     $5003
+        sta     WriteOnly-1
+        sta     WriteOnly
+        stx     WriteOnly2
+        sty     $5003
+        nop
+        bit     AlsoMultiZero-16
+        bit     NonMultiOver
+        bit     AlsoMultiZero+16
+        bit     AlsoMultiZero+224
+        nop
+        bit     MultiOne-32
+        bit     MultiOne-16
+        bit     MultiOne
+        nop
+        bit     $c002
+        bit     $c012
+        bit     $c022
+        nop
+        lda     MultiRead-1
+        lda     MultiRead
+        lda     MultiRead+1
+        lda     MultiRead+2
+        lda     $c007
+        sta     MultiWrite-1
+        sta     MultiWrite
+        sta     MultiWrite+1
+        sta     MultiWrite+2
+        sta     $c008
+        nop
+        jsr     MultiRead+1
+        nop
+        bit     MoreMultiZero
+        bit     AlsoMoreMultiZero
+        bit     MoreMultiZero+32
+        bit     MoreMultiZero+240
+        nop
+L1148   rts
 
diff --git a/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.S b/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.S
index b2b21e3..8bc9670 100644
--- a/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.S
+++ b/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.S
@@ -18,6 +18,16 @@ Over2a   =       $3006      ;$3006
 Over3    =       $3006      ;$3006-300c
 SepOver1 =       $3100      ;$3100-3103, inclusive
 SepOver2 =       $3102      ;$3102-3105, inclusive
+ReadOnly =       $5000      ;R
+WriteOnly =      $5001      ;W
+WriteOnly2 =     $5002
+MultiRead =      $c004      ;$c004/5/6, read-only
+MultiWrite =     $c005      ;$c005/6/7, write-only
+AlsoMultiZero =  $c010      ;wins (alphabetically)
+NonMultiOver =   $c010      ;winner
+MultiOne =       $c021
+MoreMultiZero =  $c100
+AlsoMoreMultiZero = $c110   ;winner
 BankWrap =       $fff0
 
 ;         .segment "SEG000"
@@ -25,9 +35,9 @@ BankWrap =       $fff0
 L1000:   lda     CodeWrap+255
          ldx     L1000
          ldy     L1000+1
-         lda     L10DD
-         lda     CodeWrap+478
-         lda     CodeWrap+485
+         lda     L1148
+         lda     CodeWrap+585
+         lda     CodeWrap+592
          nop
          lda     $1ffe
          lda     SameName1-1
@@ -104,5 +114,46 @@ LocalVar .set    $41
          lda     $4001
          lda     BankWrap+8
          lda     <BankWrap-232
-L10DD:   rts
+         nop
+         lda     ReadOnly
+         lda     ReadOnly+1
+         ldx     $5002
+         ldy     $5003
+         sta     WriteOnly-1
+         sta     WriteOnly
+         stx     WriteOnly2
+         sty     $5003
+         nop
+         bit     AlsoMultiZero-16
+         bit     NonMultiOver
+         bit     AlsoMultiZero+16
+         bit     AlsoMultiZero+224
+         nop
+         bit     MultiOne-32
+         bit     MultiOne-16
+         bit     MultiOne
+         nop
+         bit     $c002
+         bit     $c012
+         bit     $c022
+         nop
+         lda     MultiRead-1
+         lda     MultiRead
+         lda     MultiRead+1
+         lda     MultiRead+2
+         lda     $c007
+         sta     MultiWrite-1
+         sta     MultiWrite
+         sta     MultiWrite+1
+         sta     MultiWrite+2
+         sta     $c008
+         nop
+         jsr     MultiRead+1
+         nop
+         bit     MoreMultiZero
+         bit     AlsoMoreMultiZero
+         bit     MoreMultiZero+32
+         bit     MoreMultiZero+240
+         nop
+L1148:   rts
 
diff --git a/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.cfg b/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.cfg
index 76a3927..13d527a 100644
--- a/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.cfg
+++ b/SourceGen/SGTestData/Expected/2021-external-symbols_cc65.cfg
@@ -1,7 +1,7 @@
 # 6502bench SourceGen generated linker script for 2021-external-symbols
 MEMORY {
     MAIN: file=%O, start=%S, size=65536;
-#    MEM000: file=%O, start=$1000, size=222;
+#    MEM000: file=%O, start=$1000, size=329;
 }
 SEGMENTS {
     CODE: load=MAIN, type=rw;
diff --git a/SourceGen/SGTestData/Source/2021-external-symbols.S b/SourceGen/SGTestData/Source/2021-external-symbols.S
index 9982335..5e45e3d 100644
--- a/SourceGen/SGTestData/Source/2021-external-symbols.S
+++ b/SourceGen/SGTestData/Source/2021-external-symbols.S
@@ -117,4 +117,53 @@ Start   lda     Start-1     ;CodeWrap+255
         lda     $fff8       ;should be BankWrap+8
         lda     $08         ;should be BankWrap+24 or <BankWrap-232
 
+        nop
+
+; test I/O direction
+Dir     equ     $5000
+        lda     Dir
+        lda     Dir+1
+        ldx     Dir+2
+        ldy     Dir+3
+        sta     Dir
+        sta     Dir+1
+        stx     Dir+2
+        sty     Dir+3
+
+        nop
+
+; test MULTI_MASK stuff
+        bit     $c000       ;should all be AlsoMultiZero
+        bit     $c010       ;<-- except this NonMultiOver
+        bit     $c020
+        bit     $c0f0
+        nop
+        bit     $c001       ;should all be MultiOne
+        bit     $c011
+        bit     $c021
+        nop
+        bit     $c002       ;should all be hex
+        bit     $c012
+        bit     $c022
+        nop
+        lda     $c003
+        lda     $c004       ;MultiRead
+        lda     $c005       ;MultiRead+1
+        lda     $c006       ;MultiRead+2
+        lda     $c007
+        sta     $c004
+        sta     $c005       ;MultiWrite
+        sta     $c006       ;MultiWrite+1
+        sta     $c007       ;MultiWrite+2
+        sta     $c008
+        nop
+        jsr     $c005       ;MultiRead+1
+        nop
+        bit     $c100       ;should all be MoreMultiZero
+        bit     $c110       ;<-- except this AlsoMoreMultiZero
+        bit     $c120
+        bit     $c1f0
+
+        nop
+
 END     rts