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
99 changes: 96 additions & 3 deletions src/Avr8Sharp/Core/Utils/Assembler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@
return ZeroPad (r);
}},
{ "RCALL", (a, b, byteLoc, labels) => {
var k = ConstOrLabel(a, labels);
var k = ConstOrLabel(a, labels, byteLoc + 2);
if (k == int.MinValue) {
return new Func<Dictionary<string, int>, string> ((l) => OpTable?["RCALL"](a, b, byteLoc, l) as string ?? string.Empty);
}
Expand Down Expand Up @@ -561,6 +561,11 @@
[ThreadStatic]
private static SymbolTable? _currentSymbolTable;

// Thread-local byte offset of the instruction currently being encoded.
// Backs the GNU '.' location-counter operand in ConstOrLabel.
[ThreadStatic]
private static int _currentInstrByteOffset;

private readonly LabelTable _labels = new LabelTable();
private readonly List<string> _errors = new List<string>();
private readonly List<LineTablePassOne> _lines = new List<LineTablePassOne>();
Expand Down Expand Up @@ -634,6 +639,40 @@
return _errors.Count > 0 ? [] : PassTwo();
}

// -----------------------------------------------------------------------
// Strip C-style /* ... */ block comments (avr-gcc interleaves them).
// Newlines inside a block are preserved so reported line numbers stay accurate.
// String literals are respected so a "/*" inside a quoted string is left intact.
// -----------------------------------------------------------------------
private static string StripBlockComments(string input)
{
if (!input.Contains("/*")) return input;
var sb = new System.Text.StringBuilder(input.Length);
bool inString = false, inBlock = false, escaped = false;
for (int i = 0; i < input.Length; i++)
{
char c = input[i];
if (inBlock)
{
if (c == '*' && i + 1 < input.Length && input[i + 1] == '/') { inBlock = false; i++; }
else if (c == '\n') sb.Append('\n');
continue;
}
if (inString)
{
sb.Append(c);
if (escaped) escaped = false;
else if (c == '\\') escaped = true;
else if (c == '"') inString = false;
continue;
}
if (c == '"') { inString = true; sb.Append(c); continue; }
if (c == '/' && i + 1 < input.Length && input[i + 1] == '*') { inBlock = true; i++; continue; }
sb.Append(c);
}
return sb.ToString();
}

// -----------------------------------------------------------------------
// Include expansion: recursively replaces .include lines with file content
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -682,6 +721,9 @@
// -----------------------------------------------------------------------
private void PassOne (string inputData)
{
// Strip /* ... */ block comments (avr-gcc interleaves them) before line splitting.
inputData = StripBlockComments(inputData);

// Expand .include directives first
var rawLines = inputData.Split('\n');
var allLines = ExpandIncludes(rawLines);
Expand Down Expand Up @@ -770,6 +812,21 @@
_symbolTable.Set(parsed.Label, byteOffset);
}

// ----------------------------------------------------------------
// GNU-style symbol assignment: `name = expr` (no .equ/.set keyword).
// avr-gcc emits these for register aliases and helper symbols
// (e.g. `__SP_H__ = 0x3e`, `.L__stack_usage = 2`). Treated as a mutable .set.
// ----------------------------------------------------------------
if (parsed.Label == null)
{
var assignLine = LineParser.StripComments(rawLine).Trim();
if (LineParser.TryParseAssignment(assignLine, out _, out _))
{
ProcessSymbolDef(assignLine, idx, byteOffset, isImmutable: false);
continue;
}
}

// ----------------------------------------------------------------
// Dot-directives (.equ, .org, .byte, .macro, etc.)
// ----------------------------------------------------------------
Expand Down Expand Up @@ -870,6 +927,15 @@
continue;
}

// GNU/avr-gcc metadata directives: accepted as no-ops. AVR8Sharp emits a
// flat code image, so section/symbol/debug metadata carries no payload here.
// (Section switching is intentionally ignored — single-section output.)
if (dirName is "FILE" or "SECTION" or "TEXT" or "DATA" or "TYPE" or "SIZE"
or "IDENT" or "WEAK" or "GLOBL" or "LOCAL" or "ALIGN" or "P2ALIGN"
or "BALIGN" or "LOC" or "FUNC" or "ENDFUNC"
or "CFI_STARTPROC" or "CFI_ENDPROC")
continue;

// Unknown dot-directive: fall through to error
_errors.Add($"Line {idx}: Unknown directive: .{dirName}");
continue;
Expand All @@ -891,6 +957,9 @@
BytesOffset = 0
};

// Expose the current instruction address to ConstOrLabel for the GNU '.' operand.
_currentInstrByteOffset = byteOffset;

try {
switch (instruction) {
case "_REPLACE":
Expand Down Expand Up @@ -1296,9 +1365,24 @@
/// where it is most commonly found. Also, make sure it is within
/// the valid range.
/// </summary>
/// <summary>
/// Resolve a register operand to its number 0..31. Accepts the rN form and,
/// as a fallback, a numeric symbol used in register position — e.g. avr-gcc's
/// <c>__zero_reg__ = 1</c> / <c>__tmp_reg__ = 0</c>. Returns -1 if not a register.
/// </summary>
private static int ResolveRegister (string r)
{
int n = Encoders.EncoderHelpers.TryParseRegister(r.AsSpan());
if (n >= 0) return n;
var st = _currentSymbolTable;
if (st != null && st.TryGetValue(r, out var v) && v >= 0 && v <= 31)
return v;
return -1;
}

private static int DestRIndex (string r, int min = 0, int max = 31)
{
int dest = Encoders.EncoderHelpers.TryParseRegister(r.AsSpan());
int dest = ResolveRegister(r);
if (dest < 0) {
throw new Exception($"Not a register: {r}");
}
Expand All @@ -1315,7 +1399,7 @@
/// </summary>
private static int SrcRIndex (string r, int min = 0, int max = 31)
{
int dest = Encoders.EncoderHelpers.TryParseRegister(r.AsSpan());
int dest = ResolveRegister(r);
if (dest < 0) {
throw new Exception($"Not a register: {r}");
}
Expand Down Expand Up @@ -1377,6 +1461,11 @@
if (!parsed)
throw new Exception($"[Ks] out of range: {min} < {value} < {max}");

// Accept the signed spelling of an 8-bit immediate (avr-as allows -128..255 for
// LDI/SUBI/SBCI/ANDI/ORI/CPI). The encoder masks with & 0xFF, so -100 -> 0x9C.
if (d < 0 && min == 0 && max == 255 && d >= -128)
d &= 0xFF;

if (d < min || d > max) {
throw new Exception($"[Ks] out of range: {min} < {value} < {max}");
}
Expand Down Expand Up @@ -1422,6 +1511,10 @@
private static int ConstOrLabel (object value, LabelTable labels, int offset = 0)
{
if (value is not string c) return (int)value;
// GNU '.' location counter. In a relative branch, avr-as resolves '.' to the
// address of the *following* instruction (so `rjmp .` / `rcall .` encode offset 0,
// the avr-gcc stack-reserve idiom — not a self-loop). Branches here are one word.
if (c == ".") return _currentInstrByteOffset + 2 - offset;
if (labels.TryGetValue(c, out var label)) {
return label - offset;
}
Expand Down Expand Up @@ -1527,12 +1620,12 @@
public class LineTablePassOne
{
public int Line { get; set; }
public object Bytes { get; set; }

Check warning on line 1623 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Bytes' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 1623 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Bytes' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public string Text { get; set; }

Check warning on line 1624 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Text' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 1624 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Text' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public int BytesOffset { get; set; }
}

public class LineTable : LineTablePassOne
{
public new string Bytes { get; set; }

Check warning on line 1630 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Bytes' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 1630 in src/Avr8Sharp/Core/Utils/Assembler.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Non-nullable property 'Bytes' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
9 changes: 6 additions & 3 deletions src/Avr8Sharp/Core/Utils/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,17 @@ public static int Evaluate(string expr, SymbolTable symbols, int currentPc = 0)
return v;
}

// Identifiers: functions or symbols
if (tok.Kind == TokenKind.Identifier)
// Identifiers: functions or symbols.
// A Directive-kind token here is a dotted symbol reference (e.g. a forward
// reference to a GNU/avr-gcc local label like .L2) — treat it as a symbol name.
if (tok.Kind == TokenKind.Identifier || tok.Kind == TokenKind.Directive)
{
string name = tok.Raw;
bool canBeFunction = tok.Kind == TokenKind.Identifier;
pos++;

// Built-in functions
if (pos < t.Count && t[pos].Kind == TokenKind.LParen)
if (canBeFunction && pos < t.Count && t[pos].Kind == TokenKind.LParen)
{
pos++; // consume (
var arg = ParseOr(t, ref pos, sym, pc);
Expand Down
39 changes: 37 additions & 2 deletions src/Avr8Sharp/Core/Utils/LineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ public static ParsedLine Parse(string line)
return result;
}

// Try to parse label: identifier followed by ':'
if (IsIdentStart(line[pos]))
// Try to parse label: [.]identifier followed by ':'
// A leading '.' is allowed so GNU/avr-gcc local labels (.L0:, .L2:) are
// recognised as labels instead of being mistaken for dot-directives.
if (IsIdentStart(line[pos]) || (line[pos] == '.' && pos + 1 < len && IsIdentStart(line[pos + 1])))
{
int savedPos = pos;
int identStart = pos;
if (line[pos] == '.') pos++; // consume leading dot for local labels
while (pos < len && IsIdentChar(line[pos])) pos++;

if (pos < len && line[pos] == ':')
Expand Down Expand Up @@ -217,6 +220,38 @@ private static int FindFirstTopLevelComma(string s)
return -1;
}

/// <summary>
/// Detect a GNU-style symbol assignment line: <c>name = expression</c>
/// (e.g. <c>__SP_H__ = 0x3e</c> or <c>.L__stack_usage = 2</c>, as emitted by avr-gcc).
/// The name may carry a leading '.' and embedded '.' characters. Returns false for
/// instructions, directives and comparisons (a leading <c>==</c> is rejected).
/// </summary>
public static bool TryParseAssignment(string line, out string name, out string expr)
{
name = string.Empty;
expr = string.Empty;
int len = line.Length;
int pos = 0;
while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) pos++;
if (pos >= len) return false;

// LHS must be a single identifier token (letters/digits/_/.).
char first = line[pos];
if (!(char.IsLetter(first) || first == '_' || first == '.')) return false;
int nameStart = pos;
pos++;
while (pos < len && (char.IsLetterOrDigit(line[pos]) || line[pos] == '_' || line[pos] == '.')) pos++;
int nameEnd = pos;

while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) pos++;
if (pos >= len || line[pos] != '=') return false;
if (pos + 1 < len && line[pos + 1] == '=') return false; // '==' is a comparison, not an assignment

name = line[nameStart..nameEnd];
expr = line[(pos + 1)..].Trim();
return expr.Length > 0;
}

/// <summary>
/// Parse a Y+q or Z+q displacement string without regex.
/// Returns (baseReg: 'Y' or 'Z', displacement: int).
Expand Down
120 changes: 120 additions & 0 deletions tests/Avr8Sharp.Tests/AssemblerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,126 @@ public void SEZ ()
Assert.That (Bytes ("1894"), Is.EqualTo (result));
}

// -----------------------------------------------------------------------
// Regression: RCALL with a label operand computed the PC-relative offset
// from the current instruction instead of the next one (verified against
// avr-as: `loop: rcall loop` => ffdf, `rcall fwd; nop; fwd:` => 01d0).
// -----------------------------------------------------------------------
[Test(Description = "RCALL to a backward label encodes a -1 word offset (self-call)")]
public void RCALL_BackwardLabel ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("start: rcall start");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("ffdf"), Is.EqualTo (result));
}

[Test(Description = "RCALL to a forward label encodes the offset to the next instruction")]
public void RCALL_ForwardLabel ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("rcall fwd\nnop\nfwd:");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("01d00000"), Is.EqualTo (result));
}

[Test(Description = "RCALL . (GNU stack-reserve idiom) encodes offset 0")]
public void RCALL_Dot ()
{
var assembler = new AvrAssembler ();
Assert.That (Bytes ("00d0"), Is.EqualTo (assembler.Assemble ("rcall .")));
}

[Test(Description = "RJMP . encodes offset 0 (next instruction), matching avr-as")]
public void RJMP_Dot ()
{
var assembler = new AvrAssembler ();
Assert.That (Bytes ("00c0"), Is.EqualTo (assembler.Assemble ("rjmp .")));
}

// -----------------------------------------------------------------------
// avr-gcc / avr-as front-end compatibility
// -----------------------------------------------------------------------
[Test(Description = "GNU local labels (.L0:) are treated as labels, not directives")]
public void DotLocalLabel ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble (".L0: rjmp .L0");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("ffcf"), Is.EqualTo (result));
}

[Test(Description = "Forward reference to a GNU local label resolves in pass two")]
public void DotLocalLabel_ForwardRef ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("breq .L1\nnop\n.L1:");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("09f00000"), Is.EqualTo (result));
}

[Test(Description = "GNU `name = value` assignment (no .equ) defines a symbol")]
public void GnuSymbolAssignment ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("x = 0x3e\nout x, r16");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("0ebf"), Is.EqualTo (result));
}

[Test(Description = "A numeric symbol used in register position resolves to that register (avr-gcc __zero_reg__)")]
public void NumericSymbolAsRegister ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("__zero_reg__ = 1\nstd Y+1, __zero_reg__");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("1982"), Is.EqualTo (result));
}

[Test(Description = "Negative 8-bit immediates are accepted via two's complement (avr-as: ldi r16,-1 => 0fef)")]
public void NegativeImmediate ()
{
var assembler = new AvrAssembler ();
Assert.That (Bytes ("0fef"), Is.EqualTo (assembler.Assemble ("ldi r16, -1")));
var sbci = new AvrAssembler ();
Assert.That (Bytes ("9c49"), Is.EqualTo (sbci.Assemble ("sbci r25, -100")));
}

[Test(Description = "C-style /* */ block comments are stripped")]
public void BlockComment ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble ("/* set r16 */ ldi r16, 5");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("05e0"), Is.EqualTo (result));
}

[Test(Description = "avr-gcc metadata directives (.section/.size/...) are accepted as no-ops")]
public void MetadataDirectivesNoOp ()
{
var assembler = new AvrAssembler ();
var result = assembler.Assemble (".section .text\nnop\n.size main, 2");

Assert.That (assembler.Errors, Is.Empty);
Assert.That (Bytes ("0000"), Is.EqualTo (result));
}

[Test(Description = "An unknown directive is still reported as an error")]
public void UnknownDirectiveStillErrors ()
{
var assembler = new AvrAssembler ();
assembler.Assemble (".frobnicate 1");

Assert.That (assembler.Errors, Is.Not.Empty);
}

private byte[] Bytes (string hex)
{
var result = new byte[hex.Length / 2];
Expand Down
Loading