Add "StdInline" extension script

Inline strings and 16-bit addresses are sufficiently common that a
general-purpose extension script is useful.
This commit is contained in:
Andy McFadden 2021-10-16 11:36:13 -07:00
parent adf5726f62
commit 5ee01ee8a4
15 changed files with 741 additions and 24 deletions

View File

@ -82,9 +82,10 @@ namespace CommonUtil {
/// </summary>
/// <remarks>
/// MUST match Asm65.Address.NON_ADDR. We can't use the constant directly here because
/// the classes are in different packages that aren't dependent upon each other.
/// the classes are in different packages that aren't dependent upon each other. We
/// have to make this public because PluginCommon.AddressTranslate needs it as well.
/// </remarks>
private const int NON_ADDR = -1025;
public const int NON_ADDR = -1025;
#region Structural

View File

@ -15,6 +15,7 @@
*/
using System;
using System.Runtime.Serialization;
using CommonUtil;
namespace PluginCommon {
@ -37,6 +38,8 @@ namespace PluginCommon {
/// access to data that is split into multiple regions.
/// </remarks>
public class AddressTranslate {
public const int NON_ADDR = AddressMap.NON_ADDR;
private AddressMap mAddrMap;
public AddressTranslate(AddressMap addrMap) {

View File

@ -190,7 +190,11 @@ namespace PluginCommon {
IPlugin ipl = kvp.Value;
ipl.Prepare(appRef, mFileData, addrTrans);
if (ipl is IPlugin_SymbolList) {
((IPlugin_SymbolList)ipl).UpdateSymbolList(plSyms);
try {
((IPlugin_SymbolList)ipl).UpdateSymbolList(plSyms);
} catch (Exception ex) {
throw new Exception("Failed in UpdateSymbolList(" + kvp.Key + ")", ex);
}
}
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright 2021 faddenSoft
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using PluginCommon;
namespace RuntimeData.Common {
/// <summary>
/// Performs inline data formatting for various common situations:
/// <list type="bullet">
/// <item>InAZ_* - inline ASCII null-terminated string</item>
/// <item>InA1_* - inline ASCII length-delimited string</item>
/// <item>InPZ_* - inline PETSCII null-terminated string</item>
/// <item>InP1_* - inline PETSCII length-delimited string</item>
/// <item>InW_* - inline 16-bit word</item>
/// <item>InWA_* - inline 16-bit address</item>
/// <item>InNR_* - non-returning call</item>
/// </list>
/// Put a label with the appropriate prefix on the address of the subroutine, and all
/// calls to it will be formatted automatically. For example, JSRs to the label
/// "InAZ_PrintString" will be expected to be followed by null-terminated ASCII string data.
///
/// ASCII functions work for standard and high ASCII, auto-detecting the encoding based on
/// the first character.
/// </summary>
public class StdInline : MarshalByRefObject, IPlugin, IPlugin_SymbolList, IPlugin_InlineJsr {
private IApplication mAppRef;
private byte[] mFileData;
private class NameMap {
public string Prefix { get; private set; }
public InlineKind Kind { get; private set; }
public NameMap(string prefix, InlineKind kind) {
Prefix = prefix;
Kind = kind;
}
};
private enum InlineKind { Unknown = 0, InAZ, InA1, InPZ, InP1, InW, InWA, InNR };
private static NameMap[] sMap = {
new NameMap("InNR_", InlineKind.InNR),
new NameMap("InAZ_", InlineKind.InAZ),
new NameMap("InA1_", InlineKind.InA1),
new NameMap("InPZ_", InlineKind.InPZ),
new NameMap("InP1_", InlineKind.InP1),
new NameMap("InW_", InlineKind.InW),
new NameMap("InWA_", InlineKind.InWA),
};
// Map of addresses (not offsets) in project to inline data handled by code there.
private Dictionary<int, InlineKind> mInlineLabels = new Dictionary<int, InlineKind>();
// IPlugin
public string Identifier {
get { return "Standard inline data formatter"; }
}
// IPlugin
public void Prepare(IApplication appRef, byte[] fileData, AddressTranslate unused) {
mAppRef = appRef;
mFileData = fileData;
mAppRef.DebugLog("StdInline(id=" + AppDomain.CurrentDomain.Id + "): prepare()");
}
// IPlugin
public void Unprepare() {
mAppRef = null;
mFileData = null;
}
// IPlugin_SymbolList
public void UpdateSymbolList(List<PlSymbol> plSyms) {
mInlineLabels.Clear();
// Find matching symbols. Save the symbol's value (its address) and the type.
// We want an exact match on L1STR_NAME, and prefix matches on the other two.
foreach (PlSymbol sym in plSyms) {
// We might want to ignore user labels in non-addressable regions, which all
// show up with NON_ADDR as their address. In practice it doesn't matter.
foreach (NameMap map in sMap) {
if (sym.Label.StartsWith(map.Prefix)) {
// Multiple offsets could have the same address. Map the first.
if (!mInlineLabels.ContainsKey(sym.Value)) {
mInlineLabels.Add(sym.Value, map.Kind);
} else {
mAppRef.DebugLog("Ignoring duplicate address " +
sym.Value.ToString("x4"));
}
break;
}
}
}
mAppRef.DebugLog("Found matches for " + mInlineLabels.Count + " labels");
}
// IPlugin_SymbolList
public bool IsLabelSignificant(string beforeLabel, string afterLabel) {
return DoesLabelMatch(beforeLabel) || DoesLabelMatch(afterLabel);
}
private static bool DoesLabelMatch(string label) {
foreach (NameMap map in sMap) {
if (label.StartsWith(map.Prefix)) {
return true;
}
}
return false;
}
// IPlugin_InlineJsr
public void CheckJsr(int offset, int operand, out bool noContinue) {
noContinue = false;
InlineKind kind;
if (!mInlineLabels.TryGetValue(operand, out kind)) {
// JSR destination address not recognized.
return;
}
offset += 3; // move past JSR
switch (kind) {
case InlineKind.InAZ:
// Null-terminated ASCII string.
FormatNullTermString(offset, false);
break;
case InlineKind.InA1:
// Length-delimited ASCII string
FormatL1String(offset, false);
break;
case InlineKind.InPZ:
// Null-terminated PETSCII string.
FormatNullTermString(offset, true);
break;
case InlineKind.InP1:
// Length-delimited PETSCII string
FormatL1String(offset, true);
break;
case InlineKind.InW:
case InlineKind.InWA:
// 16-bit value. Start by confirming next two bytes are inside the file bounds.
if (!Util.IsInBounds(mFileData, offset, 2)) {
return;
}
if (kind == InlineKind.InW) {
// Format 16-bit value as default (hex).
mAppRef.SetInlineDataFormat(offset, 2,
DataType.NumericLE, DataSubType.None, null);
} else {
// Format 16-bit value as an address.
mAppRef.SetInlineDataFormat(offset, 2,
DataType.NumericLE, DataSubType.Address, null);
}
break;
case InlineKind.InNR:
// Non-returning call.
noContinue = true;
break;
}
}
private void FormatNullTermString(int offset, bool isPetscii) {
if (offset < 0 || offset >= mFileData.Length) {
return; // first byte is not inside file
}
// search for the terminating null byte
int nullOff = offset;
while (nullOff < mFileData.Length) {
if (mFileData[nullOff] == 0x00) {
break;
}
nullOff++;
}
if (nullOff == mFileData.Length) {
mAppRef.DebugLog("Unable to find end of null-terminated string at +" +
offset.ToString("x6"));
return;
}
DataSubType stype;
if (isPetscii) {
stype = DataSubType.C64Petscii;
} else if (mFileData[offset] >= 0x80) {
stype = DataSubType.HighAscii;
} else {
stype = DataSubType.Ascii;
}
mAppRef.SetInlineDataFormat(offset, nullOff - offset + 1,
DataType.StringNullTerm, stype, null);
}
private void FormatL1String(int offset, bool isPetscii) {
if (offset < 0 || offset >= mFileData.Length) {
return; // length byte is not inside file
}
int len = mFileData[offset];
if (offset + 1 + len > mFileData.Length) {
mAppRef.DebugLog("L1 string ran off end of file at +" + offset.ToString("x6"));
return;
}
DataSubType stype;
if (isPetscii) {
stype = DataSubType.C64Petscii;
} else if (len > 0 && mFileData[offset + 1] >= 0x80) {
stype = DataSubType.HighAscii;
} else {
stype = DataSubType.Ascii;
}
mAppRef.SetInlineDataFormat(offset, len + 1,
DataType.StringL8, stype, null);
}
}
}

Binary file not shown.

View File

@ -0,0 +1,113 @@
### 6502bench SourceGen dis65 v1.0 ###
{
"_ContentVersion":5,
"FileDataLength":210,
"FileDataCrc32":-1608872177,
"ProjectProps":{
"CpuName":"6502",
"IncludeUndocumentedInstr":false,
"TwoByteBrk":false,
"EntryFlags":32702671,
"AutoLabelStyle":"Simple",
"AnalysisParams":{
"AnalyzeUncategorizedData":true,
"DefaultTextScanMode":"LowHighAscii",
"MinCharsForString":4,
"SeekNearbyTargets":true,
"UseRelocData":false,
"SmartPlpHandling":false,
"SmartPlbHandling":true},
"PlatformSymbolFileIdentifiers":[],
"ExtensionScriptFileIdentifiers":["RT:Common/StdInline.cs"],
"ProjectSyms":{
}},
"AddressMap":[{
"Offset":0,
"Addr":4096,
"Length":-1024,
"PreLabel":"",
"IsRelative":false}],
"TypeHints":[{
"Low":0,
"High":0,
"Hint":"Code"}],
"StatusFlagOverrides":{
},
"Comments":{
},
"LongComments":{
},
"Notes":{
},
"UserLabels":{
"3":{
"Label":"InAZ_test",
"Value":4099,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"4":{
"Label":"InA1_test",
"Value":4100,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"5":{
"Label":"InPZ_test",
"Value":4101,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"6":{
"Label":"InP1_test",
"Value":4102,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"7":{
"Label":"InW_test",
"Value":4103,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"8":{
"Label":"InWA_test",
"Value":4104,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"},
"9":{
"Label":"InNR_test",
"Value":4105,
"Source":"User",
"Type":"GlobalAddr",
"LabelAnno":"None"}},
"OperandFormats":{
},
"LvTables":{
},
"Visualizations":[],
"VisualizationAnimations":[],
"VisualizationSets":{
},
"RelocList":{
},
"DbrValues":{
}}

View File

@ -0,0 +1,65 @@
.cpu "6502"
.enc "sg_hiascii"
.cdef $20,$7e,$a0
.enc "sg_ascii"
.cdef $20,$7e,$20
* = $1000
jmp L1040
InAZ_test rts
InA1_test rts
InPZ_test rts
InP1_test rts
InW_test rts
InWA_test rts
InNR_test rts
.fill 54,$00
L1040 nop
jsr InAZ_test
.null "Test AZ_ low"
jsr InAZ_test
.enc "sg_hiascii"
.null "Test AZ_ high"
jsr InA1_test
.enc "sg_ascii"
.ptext "Test A1_ low"
jsr InA1_test
.enc "sg_hiascii"
.ptext "Test A1_ high"
jsr InPZ_test
.enc "none"
.null "Test PZ",$5f
jsr InP1_test
.ptext "Test P1",$5f
jsr InW_test
.word $1234
jsr InWA_test
.word L1040
jsr _L10AD
jsr InNR_test
.byte $ea
.byte $00
_L10AD nop
jsr _L10B6
jsr _L10C3
nop
rts
_L10B6 jsr InA1_test
.byte $ff
.enc "sg_ascii"
.text "too long"
.byte $ea
_L10C3 jsr InAZ_test
.text "does not end"

View File

@ -0,0 +1,56 @@
!cpu 6502
* = $1000
jmp L1040
InAZ_test rts
InA1_test rts
InPZ_test rts
InP1_test rts
InW_test rts
InWA_test rts
InNR_test rts
!fill 54,$00
L1040 nop
jsr InAZ_test
!text "Test AZ_ low",$00
jsr InAZ_test
!hex d4e5f3f4a0c1dadfa0e8e9e7e800
jsr InA1_test
!text $0c,"Test A1_ low"
jsr InA1_test
!hex 0dd4e5f3f4a0c1b1dfa0e8e9e7e8
jsr InPZ_test
!pet "Test PZ",$5f,$00
jsr InP1_test
!pet $08,"Test P1",$5f
jsr InW_test
!word $1234
jsr InWA_test
!word L1040
jsr @L10AD
jsr InNR_test
!byte $ea
!byte $00
@L10AD nop
jsr @L10B6
jsr @L10C3
nop
rts
@L10B6 jsr InA1_test
!byte $ff
!text "too long"
!byte $ea
@L10C3 jsr InAZ_test
!text "does not end"

View File

@ -0,0 +1,56 @@
.setcpu "6502"
.org $1000
jmp L1040
InAZ_test: rts
InA1_test: rts
InPZ_test: rts
InP1_test: rts
InW_test: rts
InWA_test: rts
InNR_test: rts
.res 54,$00
L1040: nop
jsr InAZ_test
.asciiz "Test AZ_ low"
jsr InAZ_test
.byte $d4,$e5,$f3,$f4,$a0,$c1,$da,$df,$a0,$e8,$e9,$e7,$e8,$00
jsr InA1_test
.byte $0c,"Test A1_ low"
jsr InA1_test
.byte $0d,$d4,$e5,$f3,$f4,$a0,$c1,$b1,$df,$a0,$e8,$e9,$e7,$e8
jsr InPZ_test
.byte $d4,$45,$53,$54,$20,$d0,$da,$5f,$00
jsr InP1_test
.byte $08,$d4,$45,$53,$54,$20,$d0,$31,$5f
jsr InW_test
.word $1234
jsr InWA_test
.word L1040
jsr @L10AD
jsr InNR_test
.byte $ea
.byte $00
@L10AD: nop
jsr @L10B6
jsr @L10C3
nop
rts
@L10B6: jsr InA1_test
.byte $ff
.byte "too long"
.byte $ea
@L10C3: jsr InAZ_test
.byte "does not end"

View File

@ -0,0 +1,9 @@
# 6502bench SourceGen generated linker script for 20270-std-inline
MEMORY {
MAIN: file=%O, start=%S, size=65536;
}
SEGMENTS {
CODE: load=MAIN, type=rw;
}
FEATURES {}
SYMBOLS {}

View File

@ -0,0 +1,55 @@
org $1000
jmp L1040
InAZ_test rts
InA1_test rts
InPZ_test rts
InP1_test rts
InW_test rts
InWA_test rts
InNR_test rts
ds 54
L1040 nop
jsr InAZ_test
asc 'Test AZ_ low',00
jsr InAZ_test
asc "Test AZ_ high",00
jsr InA1_test
str 'Test A1_ low'
jsr InA1_test
str "Test A1_ high"
jsr InPZ_test
hex d445535420d0da5f00
jsr InP1_test
hex 08d445535420d0315f
jsr InW_test
dw $1234
jsr InWA_test
dw L1040
jsr :L10AD
jsr InNR_test
dfb $ea
dfb $00
:L10AD nop
jsr :L10B6
jsr :L10C3
nop
rts
:L10B6 jsr InA1_test
dfb $ff
asc 'too long'
dfb $ea
:L10C3 jsr InAZ_test
asc 'does not end'

View File

@ -0,0 +1,72 @@
; Copyright 2021 faddenSoft. All Rights Reserved.
; See the LICENSE.txt file for distribution terms (Apache 2.0).
;
; Test standard inline script.
;
; Assembler: ACME
; % tass64 --ascii --case-sensitive --nostart 20260-nested-regions.S
!cpu 6502
* = $1000
jmp calls
; EDIT: put appropriate labels on these
f_AZ rts
f_A1 rts
f_PZ rts
f_P1 rts
f_W rts
f_WA rts
f_NR rts
!align 63,0,0
calls nop
jsr f_AZ
!text "Test AZ_ low",$00
jsr f_AZ
!xor $80 {
!text "Test AZ_ high"
}
!byte $00
jsr f_A1
!text 12,"Test A1_ low"
jsr f_A1
!byte 13
!xor $80 {
!text "Test A1_ high"
}
jsr f_PZ
!pet "Test PZ_",$00
jsr f_P1
!pet 8,"Test P1_"
jsr f_W
!word $1234
jsr f_WA
!word calls
jsr cont
jsr f_NR
nop ;check: not formatted as instruction
brk
cont nop
; end-of-file error cases
jsr end_err1
jsr end_err2
nop
rts
end_err1
jsr f_A1
!text 255,"too long"
nop
end_err2
jsr f_AZ
!text "does not end" ;must be last

View File

@ -325,7 +325,7 @@ namespace SourceGen.Sandbox {
}
/// <summary>
/// Gathers a list of platform symbols from the project's symbol table.
/// Gathers a list of symbols from the project's symbol table.
/// </summary>
private List<PlSymbol> GeneratePlSymbolList() {
List<PlSymbol> plSymbols = new List<PlSymbol>();

View File

@ -157,10 +157,6 @@ import the changes.</p>
the full .NET Standard 2.0 APIs. They're compiled at run time by SourceGen
and executed in a sandbox with security restrictions.</p>
<p>SourceGen defines an interface that plugins must implement, and an
interface that plugins can use to interact with SourceGen. See
Interfaces.cs in the PluginCommon directory.</p>
<p>The current interfaces can be used to generate visualizations, to
identify inline data that follows JSR, JSL, or BRK instructions, and to
format operands. The latter can be used to format code and data, e.g.
@ -169,23 +165,62 @@ replacing immediate load operands with symbolic constants.</p>
<p>Scripts may be loaded from the RuntimeData directory, or from the directory
where the project file lives. Attempts to load them from other locations
will fail.</p>
<p>A project may load multiple scripts. The order in which they are
<p>A project may load multiple scripts. The order in which functions are
invoked is not defined.</p>
<h4>Known Issues and Limitations</h4>
<h3 id="built-in">Built-In Scripts</h3>
<p>Scripts are currently limited to C# version 5, because the compiler
built into .NET only handles that. C# 6 and later require installing an
additional package ("Roslyn"), so SourceGen does not support this.</p>
<p>A number of scripts are distributed with SourceGen, and may be used
freely by projects. Most are tailored for a specific platform, e.g.
Apple II ProDOS calls or Atari 2600 graphics.</p>
<p>The <samp>StdInline.cs</samp> script in the <samp>RuntimeData/Common</samp>
directory has some general-purpose inline data formatting functions.
To use them, add the script to the project, then add an appropriate label
to the subroutine that handles the inline data. For example, suppose the
code looks like this:</p>
<pre>
$1000 START JSR L1234
$1003 .STR "hello, world!"
$1010 .DD1 $00
$1011 .DD1 $a9
$1012 .DD1 $55
[...]
$1234 L1234 PLA
[...]
</pre>
<p>The code won't analyze correctly because it will try to follow the
code into the string data. If you include the script, and set the label
at <code>L1234</code> to <code>InAZ_PrintString</code>, the code will
then format correctly:</p>
<pre>
$1000 START JSR InAZ_PrintString
$1003 .ZSTR "hello, world!"
$1011 LDA #$55
[...]
$1234 InAZ_PrintString PLA
[...]
</pre>
<p>When a project is opened, any errors encountered by the script compiler
are reported to the user. If the project is already open, and a script
is added to the project through the Project Properties editor, compiler
messages are silently discarded. (This also applies if you undo/redo across
the property edit.) Use File &gt; Reload External Files to see the
compiler messages.</p>
<p>The label prefixes currently defined by the script are:</p>
<ul>
<li><code>InAZ_</code> : inline ASCII null-terminated string</li>
<li><code>InA1_</code> : inline ASCII length-delimited string</li>
<li><code>InPZ_</code> : inline PETSCII null-terminated string</li>
<li><code>InP1_</code> : inline PETSCII length-delimited string</li>
<li><code>InW_</code> : inline 16-bit word</li>
<li><code>InWA_</code> : inline 16-bit address</li>
<li><code>InNR_</code> : non-returning call (i.e. the JSR acts like
a JMP)</li>
</ul>
</p>
<h4>Development</h4>
<p>Anything more complicated will require a custom script.</p>
<h3 id="script-dev">Script Development</h3>
<p>SourceGen defines an interface that plugins must implement, and an
interface that plugins can use to interact with SourceGen. See
Interfaces.cs in the PluginCommon directory.</p>
<p>The easiest way to develop extension scripts is inside the 6502bench
solution in Visual Studio. This way you have the interfaces available
@ -205,9 +240,22 @@ will allow regeneration of the PDB files.</p>
<p>Some commonly useful functions are defined in the
<code>PluginCommon.Util</code> class, which is available to plugins. These
call into the CommonUtil library, which is shared with SourceGen.
While plugins could use CommonUtil directly, they should avoid doing so. The
APIs there are not guaranteed to be stable, so plugins that rely on them
may break in a subsequent release of SourceGen.</p>
While plugins could technically use CommonUtil directly, they should avoid
doing so. The APIs there are not guaranteed to be stable, so plugins
that rely on them may break in a subsequent release of SourceGen.</p>
<h4>Known Issues and Limitations</h4>
<p>Scripts are currently limited to C# version 5, because the compiler
built into .NET only handles that. C# 6 and later require installing an
additional package ("Roslyn"), so SourceGen does not support this.</p>
<p>When a project is opened, any errors encountered by the script compiler
are reported to the user. If the project is already open, and a script
is added to the project through the Project Properties editor, compiler
messages are silently discarded. (This also applies if you undo/redo across
the property edit.) Use File &gt; Reload External Files to see the
compiler messages.</p>
<h4>PluginDllCache Directory</h4>

View File

@ -174,7 +174,11 @@ using the <samp>Help &gt; Help</samp> menu item or by hitting
<li><a href="advanced.html">Advanced Topics</a>
<ul>
<li><a href="advanced.html#platform-symbols">Platform Symbol Files (.sym65)</a></li>
<li><a href="advanced.html#extension-scripts">Extension Scripts</a></li>
<li><a href="advanced.html#extension-scripts">Extension Scripts</a>
<ul>
<li><a href="advanced.html#built-in">Built-In Scripts</a>
<li><a href="advanced.html#script-dev">Script Development</a>
</ul></li>
<li><a href="advanced.html#multi-bin">Working With Multiple Binaries</a></li>
<li><a href="advanced.html#overlap">Overlapping Address Spaces</a></li>
<li><a href="advanced.html#reloc-data">OMF Relocation Dictionaries</a></li>