Skip to content

Commit 2eae385

Browse files
authored
Merge pull request #58 from boriel-basic/fix/sigil_variable_names_not_debuggable
Fix/sigil variable names not debuggable
2 parents fc7f772 + 674631b commit 2eae385

File tree

9 files changed

+228
-48
lines changed

9 files changed

+228
-48
lines changed

.github/workflows/build-zxbstudio.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ jobs:
2929

3030
- name: Build project
3131
run: dotnet build ZXBasicStudio.sln --configuration Release --no-restore
32+
33+
- name: Run tests
34+
run: dotnet test
3235

3336
- name: Publish for Linux
3437
run: |

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,7 @@ MigrationBackup/
360360
.ionide/
361361

362362
# Fody - auto-generated XML schema
363-
FodyWeavers.xsd
363+
FodyWeavers.xsd
364+
365+
# tmp/ dir
366+
tmp/

ZXBStudio/BuildSystem/ZXBasicMap.cs

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
179179
ParseInputParameters(funcMatch.Groups[5].Value, currentFunction.InputParameters);
180180

181181
if (funcMatch.Groups[7].Success)
182-
currentFunction.ReturnType = StorageFromString(funcMatch.Groups[5].Value, currentFunction.Name);
182+
currentFunction.ReturnType = StorageFromString(funcMatch.Groups[7].Value, currentFunction.Name);
183183
else
184184
currentFunction.ReturnType = ZXVariableStorage.F;
185185

@@ -202,8 +202,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
202202
if (varNameDef.Contains("(")) //array
203203
{
204204
string varName = varNameDef.Substring(0, varNameDef.IndexOf("(")).Trim();
205-
206-
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
205+
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
207206
continue;
208207

209208
string[] dims = varNameDef.Substring(varNameDef.IndexOf("(") + 1).Replace(")", "").Split(",", StringSplitOptions.RemoveEmptyEntries);
@@ -219,8 +218,7 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
219218
foreach (var vName in varNames)
220219
{
221220
string varName = vName.Trim();
222-
223-
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
221+
if (!jointLines.Skip(buc + 1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
224222
continue;
225223

226224
var storage = StorageFromString(dimMatch.Groups[5].Value, varName);
@@ -291,15 +289,15 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
291289
//Search for the var in the sub/function that the location points to
292290
if (location.LocationType == ZXBasicLocationType.Sub)
293291
{
294-
var sub = subs.Where(s => s.Name == location.Name).FirstOrDefault();
292+
var sub = subs.FirstOrDefault(s => string.Equals(s.Name, location.Name, StringComparison.OrdinalIgnoreCase));
295293
if(sub != null)
296-
foundVar = sub.LocalVariables.Where(v => v.Name == varName).FirstOrDefault();
294+
foundVar = sub.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase));
297295
}
298296
else
299297
{
300-
var func = functions.Where(f => f.Name == location.Name).FirstOrDefault();
298+
var func = functions.FirstOrDefault(f => string.Equals(f.Name, location.Name, StringComparison.OrdinalIgnoreCase));
301299
if (func != null)
302-
foundVar = func.LocalVariables.Where(v => v.Name == varName).FirstOrDefault();
300+
foundVar = func.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase));
303301
}
304302
}
305303

@@ -322,15 +320,15 @@ public ZXBasicMap(ZXCodeFile MainFile, IEnumerable<ZXCodeFile> AllFiles, string
322320
//(to avoid the very unprobable case where the same var is defined in different files in locations that match the same range)
323321
if (possibleLocation.LocationType == ZXBasicLocationType.Sub)
324322
{
325-
var sub = subs.Where(s => s.Name == possibleLocation.Name).FirstOrDefault();
323+
var sub = subs.FirstOrDefault(s => string.Equals(s.Name, possibleLocation.Name, StringComparison.OrdinalIgnoreCase));
326324
if (sub != null)
327-
foundVar = sub.LocalVariables.Where(v => v.Name == varName && !v.Unused).FirstOrDefault();
325+
foundVar = sub.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase) && !v.Unused);
328326
}
329327
else
330328
{
331-
var func = functions.Where(f => f.Name == possibleLocation.Name).FirstOrDefault();
329+
var func = functions.FirstOrDefault(f => string.Equals(f.Name, possibleLocation.Name, StringComparison.OrdinalIgnoreCase));
332330
if (func != null)
333-
foundVar = func.LocalVariables.Where(v => v.Name == varName && !v.Unused).FirstOrDefault();
331+
foundVar = func.LocalVariables.FirstOrDefault(v => string.Equals(v.Name, varName, StringComparison.OrdinalIgnoreCase) && !v.Unused);
334332
}
335333

336334
//If the criteria finds a var, return it
@@ -359,11 +357,7 @@ void GetSubVars(ZXBasicSub Sub, string[] Lines)
359357
if (varNameDef.Contains("(")) //array
360358
{
361359
string varName = varNameDef.Substring(0, varNameDef.IndexOf("(")).Trim();
362-
363-
//Ignore unused vars (vars that are found only on its dim line, there may be the improbable
364-
//case where a var is defined and used in the same line using a colon and not used
365-
//anywhere else, but that would be an awful code :) )
366-
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
360+
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
367361
continue;
368362

369363
string[] dims = varNameDef.Substring(varNameDef.IndexOf("(") + 1).Replace(")", "").Split(",", StringSplitOptions.RemoveEmptyEntries);
@@ -379,9 +373,7 @@ void GetSubVars(ZXBasicSub Sub, string[] Lines)
379373
foreach (var vName in varNames)
380374
{
381375
string varName = vName.Trim();
382-
383-
//Ignore unused vars
384-
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_]){varName}($|[^a-zA-Z0-9_])", RegexOptions.Multiline)))
376+
if (!Lines.Skip(buc+1).Any(l => Regex.IsMatch(l, $"(^|[^a-zA-Z0-9_$]){Regex.Escape(varName)}($|[^a-zA-Z0-9_$])", RegexOptions.Multiline)))
385377
continue;
386378

387379
var storage = StorageFromString(dimMatch.Groups[5].Value, varName);
@@ -415,15 +407,15 @@ public List<ZXBasicLocation> GetBuildLocations(ZXCodeFile CodeFile)
415407

416408
if (subMatch != null && subMatch.Success)
417409
{
418-
loc = new ZXBasicLocation { Name = subMatch.Groups[2].Value.Trim(), LocationType = ZXBasicLocationType.Sub, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
410+
loc = new ZXBasicLocation { Name = subMatch.Groups[4].Value.Trim(), LocationType = ZXBasicLocationType.Sub, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
419411
continue;
420412
}
421413

422414
var funcMatch = regFunc.Match(line);
423415

424416
if (funcMatch != null && funcMatch.Success)
425417
{
426-
loc = new ZXBasicLocation { Name = funcMatch.Groups[2].Value.Trim(), LocationType = ZXBasicLocationType.Function, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
418+
loc = new ZXBasicLocation { Name = funcMatch.Groups[4].Value.Trim(), LocationType = ZXBasicLocationType.Function, FirstLine = buc, File = Path.Combine(CodeFile.Directory, CodeFile.TempFileName) };
427419
continue;
428420
}
429421
}
@@ -465,7 +457,7 @@ public bool ContainsBuildDim(ZXCodeFile CodeFile, string VarName, int LineNumber
465457
if (LineNumber >= lines.Length)
466458
return false;
467459

468-
return Regex.IsMatch(lines[LineNumber], $"(\\s|,){VarName}(\\s|,|\\(|$)", RegexOptions.Multiline);
460+
return Regex.IsMatch(lines[LineNumber], $"(\\s|,){Regex.Escape(VarName)}(\\s|,|\\(|$)", RegexOptions.Multiline);
469461
}
470462

471463
private static void ParseInputParameters(string ParameterString, List<ZXBasicParameter> Storage)

ZXBStudio/BuildSystem/ZXVariableMap.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public ZXVariableMap(string ICFile, string MapFile, ZXBasicMap BasicMap)
4242
}
4343

4444

45-
private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
45+
internal void ProcessGlobalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
4646
{
4747
int splitIndex = icContent.IndexOf("--- end of user code ---");
4848

@@ -60,7 +60,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
6060
string varName = m.Groups[1].Value;
6161
string bVarName = varName.Substring(1);
6262

63-
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => v.Name == bVarName);
63+
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => string.Equals(v.Name.TrimEnd('$'), bVarName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));
6464

6565
if (basicVar == null)
6666
continue;
@@ -78,7 +78,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
7878

7979
ZXVariable newVar = new ZXVariable
8080
{
81-
Name = bVarName,
81+
Name = basicVar.Name,
8282
Address = new ZXVariableAddress { AddressType = ZXVariableAddressType.Absolute, AddressValue = addr },
8383
Scope = ZXVariableScope.GlobalScope,
8484
VariableType = ZXVariableType.Flat,
@@ -98,7 +98,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
9898

9999
string bVarName = varName.Substring(1);
100100

101-
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => v.Name == bVarName);
101+
var basicVar = BasicMap.GlobalVariables.FirstOrDefault(v => string.Equals(v.Name.TrimEnd('$'), bVarName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));
102102

103103
if (basicVar == null)
104104
continue;
@@ -133,7 +133,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
133133

134134
ZXVariable newVar = new ZXVariable
135135
{
136-
Name = bVarName,
136+
Name = basicVar.Name,
137137
Address = new ZXVariableAddress { AddressType = ZXVariableAddressType.Absolute, AddressValue = addr },
138138
Scope = ZXVariableScope.GlobalScope,
139139
VariableType = ZXVariableType.Array,
@@ -144,7 +144,7 @@ private void ProcessGlobalVariables(string icContent, string mapContent, ZXBasic
144144
}
145145
}
146146

147-
private void ProcessLocalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
147+
internal void ProcessLocalVariables(string icContent, string mapContent, ZXBasicMap BasicMap)
148148
{
149149
int splitIndex = icContent.IndexOf("--- end of user code ---");
150150

@@ -179,10 +179,9 @@ private void ProcessLocalVariables(string icContent, string mapContent, ZXBasicM
179179

180180
ZXVariableScope currentScope = new ZXVariableScope { ScopeName = locName, StartAddress = startAddr, EndAddress = endAddr };
181181

182-
ZXBasicSub? sub = BasicMap.Subs.Where(m => m.Name == locName).FirstOrDefault();
183-
182+
ZXBasicSub? sub = BasicMap.Subs.FirstOrDefault(m => string.Equals(m.Name.TrimEnd('$'), locName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));
184183
if (sub == null)
185-
sub = BasicMap.Functions.Where(m => m.Name == locName).FirstOrDefault();
184+
sub = BasicMap.Functions.FirstOrDefault(m => string.Equals(m.Name.TrimEnd('$'), locName.TrimEnd('$'), StringComparison.OrdinalIgnoreCase));
186185

187186
//Function params
188187
if (sub != null)

ZXBStudio/ZXBasicStudio.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
<Deterministic>False</Deterministic>
1616
<Version>1.7.0.0</Version>
1717
</PropertyGroup>
18+
19+
<ItemGroup>
20+
<InternalsVisibleTo Include="ZXBasicStudioTest" />
21+
</ItemGroup>
1822
<ItemGroup>
1923
<AvaloniaXaml Remove="LanguageDefinitions\**" />
2024
<Compile Remove="LanguageDefinitions\**" />
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
using Xunit;
2+
using FluentAssertions;
3+
using ZXBasicStudio.BuildSystem;
4+
using System.Runtime.Serialization;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace ZXBasicStudioTest
9+
{
10+
public class SigilMappingTests
11+
{
12+
[Fact]
13+
public void GetVariables_ShouldMatchSigilVariable()
14+
{
15+
// Arrange
16+
// We use GetUninitializedObject to skip the constructor which depends on files
17+
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));
18+
19+
basicMap.GlobalVariables = new[]
20+
{
21+
new ZXBasicVariable { Name = "a$" }
22+
};
23+
basicMap.Subs = new ZXBasicSub[0];
24+
basicMap.Functions = new ZXBasicFunction[0];
25+
basicMap.BuildLocations = new ZXBasicLocation[0];
26+
27+
// IC Content mimicking a global variable '_a'
28+
string icContent = "--- end of user code ---\n('var', '_a', '0')";
29+
// Map content mimicking the same variable
30+
string mapContent = "8000: ._a";
31+
32+
// Act
33+
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
34+
// We need to initialize the private 'vars' list since we used GetUninitializedObject
35+
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
36+
varsField!.SetValue(variableMap, new List<ZXVariable>());
37+
38+
variableMap.ProcessGlobalVariables(icContent, mapContent, basicMap);
39+
var variables = variableMap.Variables;
40+
41+
// Assert
42+
variables.Should().NotBeNull();
43+
var variable = variables.FirstOrDefault(v => v.Name == "a$");
44+
variable.Should().NotBeNull("Variable 'a$' should be found even if IC uses '_a'");
45+
variable!.Address.AddressValue.Should().Be(0x8000);
46+
}
47+
48+
[Fact]
49+
public void GetVariables_ShouldBeCaseInsensitive()
50+
{
51+
// Arrange
52+
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));
53+
54+
basicMap.GlobalVariables = new[]
55+
{
56+
new ZXBasicVariable { Name = "MyVar$" }
57+
};
58+
basicMap.Subs = new ZXBasicSub[0];
59+
basicMap.Functions = new ZXBasicFunction[0];
60+
basicMap.BuildLocations = new ZXBasicLocation[0];
61+
62+
// IC Content uses lowercase '_myvar'
63+
string icContent = "--- end of user code ---\n('var', '_myvar', '0')";
64+
string mapContent = "9000: ._myvar";
65+
66+
// Act
67+
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
68+
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
69+
varsField!.SetValue(variableMap, new List<ZXVariable>());
70+
71+
variableMap.ProcessGlobalVariables(icContent, mapContent, basicMap);
72+
var variables = variableMap.Variables;
73+
74+
// Assert
75+
var variable = variables.FirstOrDefault(v => v.Name == "MyVar$");
76+
variable.Should().NotBeNull("Variable 'MyVar$' should be matched case-insensitively");
77+
variable!.Address.AddressValue.Should().Be(0x9000);
78+
}
79+
80+
[Fact]
81+
public void ProcessLocalVariables_ShouldMatchSubNameWithSigil()
82+
{
83+
// Arrange
84+
var basicMap = (ZXBasicMap)FormatterServices.GetUninitializedObject(typeof(ZXBasicMap));
85+
86+
var sub = new ZXBasicSub { Name = "MySub$" };
87+
sub.LocalVariables = new List<ZXBasicVariable>();
88+
sub.InputParameters = new List<ZXBasicParameter>
89+
{
90+
new ZXBasicParameter { Name = "param1", Offset = -2, Storage = ZXVariableStorage.U16 }
91+
};
92+
93+
basicMap.GlobalVariables = new ZXBasicVariable[0];
94+
basicMap.Subs = new[] { sub };
95+
basicMap.Functions = new ZXBasicFunction[0];
96+
basicMap.BuildLocations = new[]
97+
{
98+
new ZXBasicLocation { Name = "MySub$", LocationType = ZXBasicLocationType.Sub, FirstLine = 0, LastLine = 10, File = "main.bas" }
99+
};
100+
101+
// IC Content showing start and end of MySub (sigil is stripped in label usually: _MySub)
102+
// Note: ProcessLocalVariables extracts locName = label.Substring(1) from '_MySub' -> 'MySub'
103+
string icContent = "('label', '_MySub')\n('label', '_MySub__leave')\n--- end of user code ---";
104+
// Map content showing start and end addresses
105+
string mapContent = "8000: ._MySub\n8010: ._MySub__leave";
106+
107+
// Act
108+
var variableMap = (ZXVariableMap)FormatterServices.GetUninitializedObject(typeof(ZXVariableMap));
109+
var varsField = typeof(ZXVariableMap).GetField("vars", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
110+
varsField!.SetValue(variableMap, new List<ZXVariable>());
111+
112+
variableMap.ProcessLocalVariables(icContent, mapContent, basicMap);
113+
var variables = variableMap.Variables;
114+
115+
// Assert
116+
variables.Should().NotBeNull();
117+
// If MySub$ was matched correctly, its parameters should be added
118+
variables.Should().Contain(v => v.Name == "param1", "Sub MySub$ should be matched to label _MySub and its parameters processed");
119+
var param = variables.First(v => v.Name == "param1");
120+
param.Scope.ScopeName.Should().Be("MySub");
121+
}
122+
}
123+
}

ZXBasicStudioTest/UnitTest1.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)