diff --git a/src/Avr8Sharp/Core/Utils/Assembler.cs b/src/Avr8Sharp/Core/Utils/Assembler.cs index 789bcdd..502ada4 100644 --- a/src/Avr8Sharp/Core/Utils/Assembler.cs +++ b/src/Avr8Sharp/Core/Utils/Assembler.cs @@ -399,7 +399,7 @@ public partial class AvrAssembler 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, string> ((l) => OpTable?["RCALL"](a, b, byteLoc, l) as string ?? string.Empty); } @@ -561,6 +561,11 @@ public partial class AvrAssembler [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 _errors = new List(); private readonly List _lines = new List(); @@ -634,6 +639,40 @@ public byte[] AssembleMultiFile(string[] sources) 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 // ----------------------------------------------------------------------- @@ -682,6 +721,9 @@ private List ExpandIncludes(IEnumerable rawLines, int depth = 0) // ----------------------------------------------------------------------- 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); @@ -770,6 +812,21 @@ private void PassOne (string inputData) _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.) // ---------------------------------------------------------------- @@ -870,6 +927,15 @@ private void PassOne (string inputData) 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; @@ -891,6 +957,9 @@ private void PassOne (string inputData) BytesOffset = 0 }; + // Expose the current instruction address to ConstOrLabel for the GNU '.' operand. + _currentInstrByteOffset = byteOffset; + try { switch (instruction) { case "_REPLACE": @@ -1296,9 +1365,24 @@ private int ElementSize (ref LineTablePassOne lt) /// where it is most commonly found. Also, make sure it is within /// the valid range. /// + /// + /// 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 + /// __zero_reg__ = 1 / __tmp_reg__ = 0. Returns -1 if not a register. + /// + 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}"); } @@ -1315,7 +1399,7 @@ private static int DestRIndex (string r, int min = 0, int max = 31) /// 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}"); } @@ -1377,6 +1461,11 @@ private static int ConstValue (string value, int min = 0, int max = 255) 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}"); } @@ -1422,6 +1511,10 @@ private static int FitTwoC (int r, int bits) 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; } diff --git a/src/Avr8Sharp/Core/Utils/ExpressionEvaluator.cs b/src/Avr8Sharp/Core/Utils/ExpressionEvaluator.cs index c6067d9..6ee3e02 100644 --- a/src/Avr8Sharp/Core/Utils/ExpressionEvaluator.cs +++ b/src/Avr8Sharp/Core/Utils/ExpressionEvaluator.cs @@ -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); diff --git a/src/Avr8Sharp/Core/Utils/LineParser.cs b/src/Avr8Sharp/Core/Utils/LineParser.cs index 6d1930d..13838b1 100644 --- a/src/Avr8Sharp/Core/Utils/LineParser.cs +++ b/src/Avr8Sharp/Core/Utils/LineParser.cs @@ -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] == ':') @@ -217,6 +220,38 @@ private static int FindFirstTopLevelComma(string s) return -1; } + /// + /// Detect a GNU-style symbol assignment line: name = expression + /// (e.g. __SP_H__ = 0x3e or .L__stack_usage = 2, as emitted by avr-gcc). + /// The name may carry a leading '.' and embedded '.' characters. Returns false for + /// instructions, directives and comparisons (a leading == is rejected). + /// + 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; + } + /// /// Parse a Y+q or Z+q displacement string without regex. /// Returns (baseReg: 'Y' or 'Z', displacement: int). diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 70d3d0a..3e9846b 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -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];