From 5ef50042a2ee849afa90f3236b5169df634e4914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:18:03 -0600 Subject: [PATCH 1/6] refactor(assembler): remove ~1000 lines of dead, unwired encoder modules PR #6 introduced parallel encoder/directive modules but never wired them into the live assembly path, which is still the legacy OpTable plus inline directive handling in Assembler.cs. The duplication is what allowed the RCALL offset bug to be correct in BranchEncoders.cs yet wrong in the live OpTable. Removed (0 external references, verified by grep across src/ and tests/): - Encoders/{Branch,Arithmetic,LoadStore,BitManip,Misc}Encoders.cs - DirectiveProcessor.cs - AssemblerContext.cs Kept (live): EncoderHelpers.cs (register parsing), Tokenizer.cs, AssemblerIr.cs (SymbolTable/LabelTable/LineTablePassOne). 484 tests pass, no behaviour change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Avr8Sharp/Core/Utils/AssemblerContext.cs | 49 -- .../Core/Utils/DirectiveProcessor.cs | 460 ------------------ .../Core/Utils/Encoders/ArithmeticEncoders.cs | 153 ------ .../Core/Utils/Encoders/BitManipEncoders.cs | 92 ---- .../Core/Utils/Encoders/BranchEncoders.cs | 70 --- .../Core/Utils/Encoders/LoadStoreEncoders.cs | 158 ------ .../Core/Utils/Encoders/MiscEncoders.cs | 30 -- 7 files changed, 1012 deletions(-) delete mode 100644 src/Avr8Sharp/Core/Utils/AssemblerContext.cs delete mode 100644 src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs delete mode 100644 src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs delete mode 100644 src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs delete mode 100644 src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs delete mode 100644 src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs delete mode 100644 src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs diff --git a/src/Avr8Sharp/Core/Utils/AssemblerContext.cs b/src/Avr8Sharp/Core/Utils/AssemblerContext.cs deleted file mode 100644 index 9cbba26..0000000 --- a/src/Avr8Sharp/Core/Utils/AssemblerContext.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace AVR8Sharp.Core.Utils; - -public enum DiagnosticSeverity { Error, Warning } - -/// -/// Structured assembler diagnostic, compatible with avr-as output format. -/// -public record AssemblerDiagnostic( - DiagnosticSeverity Severity, - string Message, - string? SourceFile, - int Line, - int Column = 0) -{ - public override string ToString() - { - string loc = SourceFile != null - ? $"{SourceFile}:{Line}:{Column}" - : $":{Line}:{Column}"; - string sev = Severity == DiagnosticSeverity.Error ? "error" : "warning"; - return $"{loc}: {sev}: {Message}"; - } -} - -/// -/// Assembler context holding all state that persists through both passes. -/// -internal class AssemblerContext -{ - public SymbolTable Symbols { get; } = new SymbolTable(); - public List Lines { get; } = new List(); - public List Diagnostics { get; } = new List(); - public List Fixups { get; } = new List(); - - public bool HasErrors => Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error); - - public void Error(string message, int line, string? file = null, int column = 0) => - Diagnostics.Add(new AssemblerDiagnostic(DiagnosticSeverity.Error, message, file, line, column)); - - public void Warning(string message, int line, string? file = null, int column = 0) => - Diagnostics.Add(new AssemblerDiagnostic(DiagnosticSeverity.Warning, message, file, line, column)); - - public void Reset() - { - Lines.Clear(); - Diagnostics.Clear(); - Fixups.Clear(); - } -} diff --git a/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs b/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs deleted file mode 100644 index f5689bb..0000000 --- a/src/Avr8Sharp/Core/Utils/DirectiveProcessor.cs +++ /dev/null @@ -1,460 +0,0 @@ -namespace AVR8Sharp.Core.Utils; - -/// -/// Segment type for multi-segment support. -/// -public enum SegmentType { Code, Data, Eeprom } - -/// -/// Processes assembler directives during pass 1. -/// -internal class DirectiveProcessor -{ - private readonly SymbolTable _symbols; - private readonly Dictionary Params, List Body)> _macros = new(StringComparer.OrdinalIgnoreCase); - - // Segment location counters (byte addresses) - private int _codeOffset; - private int _dataOffset; - private int _eepromOffset; - private SegmentType _currentSegment = SegmentType.Code; - - // Conditional assembly stack: true = currently assembling - private readonly Stack _condStack = new(); - private bool _assembling = true; // top-level: always assemble - - // Macro recording state - private string? _recordingMacro; - private List? _recordingParams; - private List? _recordingBody; - - // Include file resolver - private readonly Func? _fileResolver; - - public int CurrentOffset => _currentSegment switch - { - SegmentType.Code => _codeOffset, - SegmentType.Data => _dataOffset, - SegmentType.Eeprom => _eepromOffset, - _ => _codeOffset - }; - - public SegmentType CurrentSegment => _currentSegment; - public bool Assembling => _assembling; - - public DirectiveProcessor(SymbolTable symbols, Func? fileResolver = null) - { - _symbols = symbols; - _fileResolver = fileResolver; - } - - public void SetCodeOffset(int offset) => _codeOffset = offset; - public void AdvanceCodeOffset(int bytes) => _codeOffset += bytes; - - /// - /// Process a directive line. Returns true if the line was a directive (and thus consumed), - /// false if it is not a directive. Also handles macro expansion by returning lines to insert. - /// - public bool ProcessDirective(string name, string args, out List? extraLines, out List emittedLines, out string? error) - { - extraLines = null; - emittedLines = new List(); - error = null; - - // Handle macro recording - if (_recordingMacro != null) - { - string upper = name.ToUpperInvariant(); - if (upper == ".ENDMACRO" || upper == ".ENDM" || upper == "ENDMACRO" || upper == "ENDM") - { - _macros[_recordingMacro] = (_recordingParams!, _recordingBody!); - _recordingMacro = null; - _recordingParams = null; - _recordingBody = null; - } - else - { - _recordingBody!.Add(string.IsNullOrEmpty(args) ? name : $"{name} {args}"); - } - return true; - } - - string directive = name.TrimStart('.').ToUpperInvariant(); - - switch (directive) - { - case "CSEG": - _currentSegment = SegmentType.Code; - return true; - case "DSEG": - _currentSegment = SegmentType.Data; - return true; - case "ESEG": - _currentSegment = SegmentType.Eeprom; - return true; - - case "ORG": - case "LOC": - { - string expr = args.Trim(); - int? val = ExpressionEvaluator.TryEvaluate(expr, _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate expression: {expr}"; return true; } - if ((val.Value & 1) != 0 && _currentSegment == SegmentType.Code) { error = "Code origin must be even"; return true; } - switch (_currentSegment) - { - case SegmentType.Code: _codeOffset = val.Value; break; - case SegmentType.Data: _dataOffset = val.Value; break; - case SegmentType.Eeprom: _eepromOffset = val.Value; break; - } - return true; - } - - case "EQU": - { - // .equ NAME = VALUE - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".equ requires NAME = VALUE"; return true; } - string symName = parts[0].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(parts[1].Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate .equ expression for {symName}"; return true; } - try { _symbols.DefineConst(symName, val.Value); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "SET": - { - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".set requires NAME = VALUE"; return true; } - string symName = parts[0].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(parts[1].Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate .set expression for {symName}"; return true; } - try { _symbols.DefineVar(symName, val.Value); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "DEF": - { - // .def ALIAS = rN - var parts = args.Split('=', 2); - if (parts.Length != 2) { error = ".def requires ALIAS = rN"; return true; } - string alias = parts[0].Trim(); - string regStr = parts[1].Trim(); - int n = Encoders.EncoderHelpers.TryParseRegister(regStr.AsSpan()); - if (n < 0) { error = $".def: right side must be a register, got '{regStr}'"; return true; } - try { _symbols.DefineConst(alias, n); } - catch (Exception ex) { error = ex.Message; } - return true; - } - - case "REPLACE": - { - // backward-compat: _REPLACE NAME, VALUE (no '=') - var parts = args.Split(',', 2); - if (parts.Length != 2) { error = "_REPLACE requires NAME, VALUE"; return true; } - string symName = parts[0].Trim(); - string valStr = parts[1].Trim(); - int? val = ExpressionEvaluator.TryEvaluate(valStr, _symbols, _codeOffset); - if (val == null) - { - // store as raw text for later text-substitution (backward-compat mode) - _symbols.Set(symName, 0); // placeholder - return true; - } - try { _symbols.Set(symName, val.Value); } - catch { /* ignore redefinition in backward-compat mode */ } - return true; - } - - case "BYTE": - case "DB": - { - if (_currentSegment != SegmentType.Code) return true; // DSEG .byte just reserves space - var bytes = EmitCommaSeparatedBytes(args, out error); - if (bytes != null && bytes.Length > 0) - { - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = $".{directive.ToLowerInvariant()} {args}", - Line = 0 - }); - _codeOffset += bytes.Length; - } - return true; - } - - case "WORD": - case "DW": - case "IW": // backward-compat _IW - { - var words = EmitCommaSeparatedWords(args, out error); - if (words != null && words.Length > 0) - { - var wordBytes = new byte[words.Length * 2]; - for (int i = 0; i < words.Length; i++) - { - wordBytes[i * 2] = (byte)(words[i] & 0xFF); - wordBytes[i * 2 + 1] = (byte)((words[i] >> 8) & 0xFF); - } - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(wordBytes), - Text = $".{directive.ToLowerInvariant()} {args}", - Line = 0 - }); - _codeOffset += wordBytes.Length; - } - return true; - } - - case "DWORD": - { - var words = EmitCommaSeparatedWords(args, out error); - if (words != null && words.Length > 0) - { - var dwordBytes = new byte[words.Length * 4]; - for (int i = 0; i < words.Length; i++) - { - dwordBytes[i * 4] = (byte)(words[i] & 0xFF); - dwordBytes[i * 4 + 1] = (byte)((words[i] >> 8) & 0xFF); - dwordBytes[i * 4 + 2] = (byte)((words[i] >> 16) & 0xFF); - dwordBytes[i * 4 + 3] = (byte)((words[i] >> 24) & 0xFF); - } - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(dwordBytes), - Text = ".dword " + args, - Line = 0 - }); - _codeOffset += dwordBytes.Length; - } - return true; - } - - case "ASCII": - { - string str = UnquoteString(args.Trim()); - var bytes = System.Text.Encoding.ASCII.GetBytes(str); - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = ".ascii " + args, - Line = 0 - }); - _codeOffset += bytes.Length; - return true; - } - - case "ASCIZ": - case "STRING": - { - string str = UnquoteString(args.Trim()); - var rawBytes = System.Text.Encoding.ASCII.GetBytes(str); - var bytes = new byte[rawBytes.Length + 1]; - rawBytes.CopyTo(bytes, 0); - bytes[^1] = 0; - emittedLines.Add(new AsmLine - { - BytesOffset = _codeOffset, - Encoding = new InstructionWord.DataEncoding(bytes), - Text = ".asciz " + args, - Line = 0 - }); - _codeOffset += bytes.Length; - return true; - } - - case "IF": - { - int? val = ExpressionEvaluator.TryEvaluate(args.Trim(), _symbols, _codeOffset); - bool result = val.HasValue && val.Value != 0; - _condStack.Push(_assembling); - _assembling = _assembling && result; - return true; - } - - case "IFDEF": - { - bool defined = _symbols.ContainsKey(args.Trim()); - _condStack.Push(_assembling); - _assembling = _assembling && defined; - return true; - } - - case "IFNDEF": - { - bool notDefined = !_symbols.ContainsKey(args.Trim()); - _condStack.Push(_assembling); - _assembling = _assembling && notDefined; - return true; - } - - case "ELSEIF": - { - if (_condStack.Count == 0) { error = ".elseif without .if"; return true; } - bool wasActive = _condStack.Peek(); - if (_assembling) - { - // We were assembling the if-branch; now skip - _assembling = false; - } - else if (wasActive) - { - // Parent was active; evaluate this branch - int? val = ExpressionEvaluator.TryEvaluate(args.Trim(), _symbols, _codeOffset); - _assembling = val.HasValue && val.Value != 0; - } - return true; - } - - case "ELSE": - { - if (_condStack.Count == 0) { error = ".else without .if"; return true; } - bool parentActive = _condStack.Peek(); - _assembling = parentActive && !_assembling; - return true; - } - - case "ENDIF": - { - if (_condStack.Count == 0) { error = ".endif without .if"; return true; } - _assembling = _condStack.Pop(); - return true; - } - - case "MACRO": - { - var parts = args.Split(new[] { ' ', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) { error = ".macro requires a name"; return true; } - _recordingMacro = parts[0].Trim(); - _recordingParams = parts.Skip(1).Select(p => p.Trim()).ToList(); - _recordingBody = new List(); - return true; - } - - case "ENDMACRO": - case "ENDM": - { - error = ".endm without .macro"; - return true; - } - - case "INCLUDE": - { - string filename = UnquoteString(args.Trim()); - if (_fileResolver == null) { error = ".include requires a file resolver"; return true; } - try - { - string content = _fileResolver(filename); - extraLines = content.Split('\n').ToList(); - } - catch (Exception ex) - { - error = $".include error: {ex.Message}"; - } - return true; - } - - default: - return false; // Not a directive we handle - } - } - - /// - /// Try to expand a macro by name. Returns the expanded lines or null if not a macro. - /// - public List? TryExpandMacro(string name, string args, int depth = 0) - { - if (depth > 8) throw new Exception($"Macro recursion limit exceeded in '{name}'"); - if (!_macros.TryGetValue(name, out var macro)) return null; - - var argValues = args.Split(',').Select(a => a.Trim()).ToArray(); - var expanded = new List(); - - foreach (var line in macro.Body) - { - string expandedLine = line; - for (int i = 0; i < macro.Params.Count; i++) - { - if (i < argValues.Length) - { - expandedLine = expandedLine.Replace(@"\" + macro.Params[i], argValues[i]); - expandedLine = expandedLine.Replace("@" + i, argValues[i]); - } - } - expanded.Add(expandedLine); - } - return expanded; - } - - private byte[]? EmitCommaSeparatedBytes(string args, out string? error) - { - error = null; - var parts = SplitArgs(args); - var bytes = new List(); - foreach (var p in parts) - { - string trimmed = p.Trim(); - // String literals - if (trimmed.StartsWith('"')) - { - string str = UnquoteString(trimmed); - bytes.AddRange(System.Text.Encoding.ASCII.GetBytes(str)); - continue; - } - int? val = ExpressionEvaluator.TryEvaluate(trimmed, _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate: {trimmed}"; return null; } - bytes.Add((byte)(val.Value & 0xFF)); - } - return bytes.ToArray(); - } - - private int[]? EmitCommaSeparatedWords(string args, out string? error) - { - error = null; - var parts = SplitArgs(args); - var words = new List(); - foreach (var p in parts) - { - int? val = ExpressionEvaluator.TryEvaluate(p.Trim(), _symbols, _codeOffset); - if (val == null) { error = $"Cannot evaluate: {p.Trim()}"; return null; } - words.Add(val.Value); - } - return words.ToArray(); - } - - private static List SplitArgs(string args) - { - // Split by comma, but not inside quotes - var parts = new List(); - int depth = 0; - bool inString = false; - var cur = new System.Text.StringBuilder(); - foreach (char c in args) - { - if (c == '"') inString = !inString; - if (!inString) - { - if (c == '(') depth++; - else if (c == ')') depth--; - else if (c == ',' && depth == 0) { parts.Add(cur.ToString()); cur.Clear(); continue; } - } - cur.Append(c); - } - if (cur.Length > 0) parts.Add(cur.ToString()); - return parts; - } - - private static string UnquoteString(string s) - { - if (s.StartsWith('"') && s.EndsWith('"') && s.Length >= 2) - return s[1..^1]; - return s; - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs deleted file mode 100644 index 9992f20..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/ArithmeticEncoders.cs +++ /dev/null @@ -1,153 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class ArithmeticEncoders -{ - public static ushort ADD(string rd, string rr) => - (ushort)(0x0C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ADC(string rd, string rr) => - (ushort)(0x1C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ADIW(string rd, string k) - { - int d = ParseWordRegPair(rd); - int imm = ParseImm(k, 0, 63); - return (ushort)(0x9600 | (d & 0x3) << 4 | ((imm & 0x30) << 2) | (imm & 0x0F)); - } - - public static ushort AND(string rd, string rr) => - (ushort)(0x2000 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ANDI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x7000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort COM(string rd) => - (ushort)(0x9400 | DestRd(ParseRegister(rd))); - - public static ushort CP(string rd, string rr) => - (ushort)(0x1400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort CPC(string rd, string rr) => - (ushort)(0x0400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort CPI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x3000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort CPSE(string rd, string rr) => - (ushort)(0x1000 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort DEC(string rd) => - (ushort)(0x940A | DestRd(ParseRegister(rd))); - - public static ushort EOR(string rd, string rr) => - (ushort)(0x2400 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort FMUL(string rd, string rr) => - (ushort)(0x0308 | DestRd(ParseRegister(rd, 16, 23)) | (SrcRr(ParseRegister(rr, 16, 23)) & 0x7)); - - public static ushort FMULS(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0380 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort FMULSU(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0388 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort INC(string rd) => - (ushort)(0x9403 | DestRd(ParseRegister(rd))); - - public static ushort MOV(string rd, string rr) => - (ushort)(0x2C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort MOVW(string rd, string rr) - { - int d = ParseRegister(rd); - int r = ParseRegister(rr); - return (ushort)(0x0100 | ((d >> 1) & 0xF) << 4 | ((r >> 1) & 0xF)); - } - - public static ushort MUL(string rd, string rr) => - (ushort)(0x9C00 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort MULS(string rd, string rr) - { - int d = ParseRegister(rd, 16, 31); - int r = ParseRegister(rr, 16, 31); - return (ushort)(0x0200 | ((d & 0xF) << 4) | (r & 0xF)); - } - - public static ushort MULSU(string rd, string rr) - { - int d = ParseRegister(rd, 16, 23); - int r = ParseRegister(rr, 16, 23); - return (ushort)(0x0300 | ((d & 0x7) << 4) | (r & 0x7)); - } - - public static ushort NEG(string rd) => - (ushort)(0x9401 | DestRd(ParseRegister(rd))); - - public static ushort OR(string rd, string rr) => - (ushort)(0x2800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort ORI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x6000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort SBC(string rd, string rr) => - (ushort)(0x0800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort SBCI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x4000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static ushort SBIW(string rd, string k) - { - int d = ParseWordRegPair(rd); - int imm = ParseImm(k, 0, 63); - return (ushort)(0x9700 | (d & 0x3) << 4 | ((imm & 0x30) << 2) | (imm & 0x0F)); - } - - public static ushort SUB(string rd, string rr) => - (ushort)(0x1800 | DestRd(ParseRegister(rd)) | SrcRr(ParseRegister(rr))); - - public static ushort SUBI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0x5000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - // Pseudo-instructions - public static ushort CLR(string rd) => EOR(rd, rd); - public static ushort LSL(string rd) => ADD(rd, rd); - public static ushort ROL(string rd) => ADC(rd, rd); - public static ushort TST(string rd) => AND(rd, rd); - public static ushort SBR(string rd, string k) => ORI(rd, k); - public static ushort CRB(string rd, string k) - { - int kv = ParseValue(k); - return ANDI(rd, (~kv & 0xFF).ToString()); - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs deleted file mode 100644 index 88b5193..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/BitManipEncoders.cs +++ /dev/null @@ -1,92 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class BitManipEncoders -{ - public static ushort BCLR(string s) - { - int bit = ParseImm(s, 0, 7); - return (ushort)(0x9488 | (bit << 4)); - } - - public static ushort BSET(string s) - { - int bit = ParseImm(s, 0, 7); - return (ushort)(0x9408 | (bit << 4)); - } - - public static ushort BLD(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xF800 | DestRd(d) | bit); - } - - public static ushort BST(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFA00 | DestRd(d) | bit); - } - - public static ushort CBI(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9800 | (a << 3) | bit); - } - - public static ushort SBI(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9A00 | (a << 3) | bit); - } - - public static ushort SBIC(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9900 | (a << 3) | bit); - } - - public static ushort SBIS(string A, string b) - { - int a = ParseImm(A, 0, 31); - int bit = ParseImm(b, 0, 7); - return (ushort)(0x9B00 | (a << 3) | bit); - } - - public static ushort SBRC(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFC00 | DestRd(d) | bit); - } - - public static ushort SBRS(string rd, string b) - { - int d = ParseRegister(rd); - int bit = ParseImm(b, 0, 7); - return (ushort)(0xFE00 | DestRd(d) | bit); - } - - // Flag set/clear - public static ushort CLC() => BCLR("0"); - public static ushort CLH() => BCLR("5"); - public static ushort CLI() => BCLR("7"); - public static ushort CLN() => BCLR("2"); - public static ushort CLS() => BCLR("4"); - public static ushort CLT() => BCLR("6"); - public static ushort CLV() => BCLR("3"); - public static ushort CLZ() => BCLR("1"); - public static ushort SEC() => BSET("0"); - public static ushort SEH() => BSET("5"); - public static ushort SEI() => BSET("7"); - public static ushort SEN() => BSET("2"); - public static ushort SES() => BSET("4"); - public static ushort SET() => BSET("6"); - public static ushort SEV() => BSET("3"); - public static ushort SEZ() => BSET("1"); -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs deleted file mode 100644 index fbdb5a0..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/BranchEncoders.cs +++ /dev/null @@ -1,70 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class BranchEncoders -{ - public static (ushort, ushort) CALL(int k) - { - k = k >> 1; - int hk = (k >> 16) & 0x3F; - int lk = k & 0xFFFF; - ushort high = (ushort)(0x940E | ((hk & 0x3E) << 3) | (hk & 1)); - return (high, (ushort)lk); - } - - public static (ushort, ushort) JMP(int k) - { - k = k >> 1; - int hk = (k >> 16) & 0x3F; - int lk = k & 0xFFFF; - ushort high = (ushort)(0x940C | ((hk & 0x3E) << 3) | (hk & 1)); - return (high, (ushort)lk); - } - - public static ushort RCALL(int k) - { - int offset = k >> 1; // k is byte offset from NEXT instruction - return (ushort)(0xD000 | FitTwoC(offset, 12)); - } - - public static ushort RJMP(int k) - { - int offset = k >> 1; - return (ushort)(0xC000 | FitTwoC(offset, 12)); - } - - public static ushort BRBC(string s, int k) - { - int bit = EncoderHelpers.ParseImm(s, 0, 7); - int offset = FitTwoC(k >> 1, 7); - return (ushort)(0xF400 | bit | (offset << 3)); - } - - public static ushort BRBS(string s, int k) - { - int bit = EncoderHelpers.ParseImm(s, 0, 7); - int offset = FitTwoC(k >> 1, 7); - return (ushort)(0xF000 | bit | (offset << 3)); - } - - // Flag-specific branches (delegate to BRBC/BRBS) - public static ushort BRCC(int k) => BRBC("0", k); - public static ushort BRCS(int k) => BRBS("0", k); - public static ushort BREQ(int k) => BRBS("1", k); - public static ushort BRGE(int k) => BRBC("4", k); - public static ushort BRHC(int k) => BRBC("5", k); - public static ushort BRHS(int k) => BRBS("5", k); - public static ushort BRID(int k) => BRBC("7", k); - public static ushort BRIE(int k) => BRBS("7", k); - public static ushort BRLO(int k) => BRBS("0", k); - public static ushort BRLT(int k) => BRBS("4", k); - public static ushort BRMI(int k) => BRBS("2", k); - public static ushort BRNE(int k) => BRBC("1", k); - public static ushort BRPL(int k) => BRBC("2", k); - public static ushort BRSH(int k) => BRBC("0", k); - public static ushort BRTC(int k) => BRBC("6", k); - public static ushort BRTS(int k) => BRBS("6", k); - public static ushort BRVC(int k) => BRBC("3", k); - public static ushort BRVS(int k) => BRBS("3", k); -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs deleted file mode 100644 index 3ca937c..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/LoadStoreEncoders.cs +++ /dev/null @@ -1,158 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class LoadStoreEncoders -{ - - public static ushort LD(string rd, string src) - { - int d = ParseRegister(rd); - int mode = StldXyz(src); - return (ushort)(DestRd(d) | mode); - } - - public static ushort LDD(string rd, string src) - { - int d = ParseRegister(rd); - int mode = StldYzQ(src); - return (ushort)(DestRd(d) | mode); - } - - public static ushort LDI(string rd, string k) - { - int d = ParseRegister(rd, 16, 31); - int imm = ParseImm(k, 0, 255); - return (ushort)(0xE000 | (DestRd(d) & 0xF0) | ((imm & 0xF0) << 4) | (imm & 0x0F)); - } - - public static (ushort, ushort) LDS(string rd, string k) - { - int d = ParseRegister(rd); - int addr = ParseImm(k, 0, 65535); - return ((ushort)(0x9000 | DestRd(d)), (ushort)addr); - } - - public static ushort LPM(string rd, string src) - { - if (string.IsNullOrEmpty(rd)) return 0x95C8; - int d = ParseRegister(rd); - int op = src switch { "Z" => 4, "Z+" => 5, _ => throw new Exception("LPM: second operand must be Z or Z+") }; - return (ushort)(0x9000 | DestRd(d) | op); - } - - public static ushort ELPM(string rd, string src) - { - if (string.IsNullOrEmpty(rd)) return 0x95D8; - int d = ParseRegister(rd); - int op = src switch { "Z" => 6, "Z+" => 7, _ => throw new Exception("ELPM: second operand must be Z or Z+") }; - return (ushort)(0x9000 | DestRd(d) | op); - } - - public static ushort LSR(string rd) => - (ushort)(0x9406 | DestRd(ParseRegister(rd))); - - public static ushort ASR(string rd) => - (ushort)(0x9405 | DestRd(ParseRegister(rd))); - - public static ushort ROR(string rd) => - (ushort)(0x9407 | DestRd(ParseRegister(rd))); - - public static ushort SWAP(string rd) => - (ushort)(0x9402 | DestRd(ParseRegister(rd))); - - public static ushort POP(string rd) => - (ushort)(0x900F | DestRd(ParseRegister(rd))); - - public static ushort PUSH(string rd) => - (ushort)(0x920F | DestRd(ParseRegister(rd))); - - public static ushort ST(string dst, string rr) - { - int r = ParseRegister(rr); - int mode = StldXyz(dst); - return (ushort)(0x0200 | DestRd(r) | mode); - } - - public static ushort STD(string dst, string rr) - { - int r = ParseRegister(rr); - int mode = StldYzQ(dst); - return (ushort)(0x0200 | DestRd(r) | mode); - } - - public static (ushort, ushort) STS(string addr, string rr) - { - int r = ParseRegister(rr); - int k = ParseImm(addr, 0, 65535); - return ((ushort)(0x9200 | DestRd(r)), (ushort)k); - } - - public static ushort IN(string rd, string A) - { - int d = ParseRegister(rd); - int a = ParseImm(A, 0, 63); - return (ushort)(0xB000 | DestRd(d) | ((a & 0x30) << 5) | (a & 0x0F)); - } - - public static ushort OUT(string A, string rr) - { - int r = ParseRegister(rr); - int a = ParseImm(A, 0, 63); - return (ushort)(0xB800 | DestRd(r) | ((a & 0x30) << 5) | (a & 0x0F)); - } - - public static ushort LAC(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9206 | DestRd(ParseRegister(rd))); - } - - public static ushort LAS(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9205 | DestRd(ParseRegister(rd))); - } - - public static ushort LAT(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9207 | DestRd(ParseRegister(rd))); - } - - public static ushort XCH(string z, string rd) - { - if (!z.Equals("Z", StringComparison.OrdinalIgnoreCase)) throw new Exception("First operand must be Z"); - return (ushort)(0x9204 | DestRd(ParseRegister(rd))); - } - - public static ushort SER(string rd) - { - int d = ParseRegister(rd, 16, 31); - return (ushort)(0xEF0F | (DestRd(d) & 0xF0)); - } - - private static int StldXyz(string xyz) => xyz.ToUpperInvariant() switch - { - "X" => 0x900C, - "X+" => 0x900D, - "-X" => 0x900E, - "Y" => 0x8008, - "Y+" => 0x9009, - "-Y" => 0x900A, - "Z" => 0x8000, - "Z+" => 0x9001, - "-Z" => 0x9002, - _ => throw new Exception($"Not a valid indirect address mode: {xyz}") - }; - - private static int StldYzQ(string yzq) - { - var (baseReg, q) = LineParser.ParseYzDisplacement(yzq); - if (q < 0 || q > 63) throw new Exception("Displacement q out of range 0..63"); - int r = 0x8000; - if (baseReg == 'Y') r |= 0x8; - r |= ((q & 0x20) << 8) | ((q & 0x18) << 7) | (q & 0x7); - return r; - } -} diff --git a/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs b/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs deleted file mode 100644 index 7e8cae3..0000000 --- a/src/Avr8Sharp/Core/Utils/Encoders/MiscEncoders.cs +++ /dev/null @@ -1,30 +0,0 @@ -using static AVR8Sharp.Core.Utils.Encoders.EncoderHelpers; - -namespace AVR8Sharp.Core.Utils.Encoders; - -internal static class MiscEncoders -{ - public static ushort NOP() => 0x0000; - public static ushort RET() => 0x9508; - public static ushort RETI() => 0x9518; - public static ushort SLEEP() => 0x9588; - public static ushort WDR() => 0x95A8; - public static ushort BREAK() => 0x9598; - public static ushort ICALL() => 0x9509; - public static ushort IJMP() => 0x9409; - public static ushort EICALL() => 0x9519; - public static ushort EIJMP() => 0x9419; - - public static ushort DES(string k) - { - int imm = ParseImm(k, 0, 15); - return (ushort)(0x940B | (imm << 4)); - } - - public static ushort SPM(string? operand = null) - { - if (string.IsNullOrEmpty(operand)) return 0x95E8; - if (operand.Trim().ToUpperInvariant() == "Z+") return 0x95F8; - throw new Exception("SPM operand must be empty or Z+"); - } -} From 6077f5ec0fb39ec5257ba746c72164bcdd340165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:21:08 -0600 Subject: [PATCH 2/6] feat(assembler): 1-based error line numbers + TryAssemble/AssembleOrThrow - Error messages used the 0-based line index (`Line {idx}`) while the line table is 1-based, so every diagnostic was off by one. All sites now report 1-based line numbers. Updated the one test that pinned the old value. - Add TryAssemble(input, out bytes, out errors) and AssembleOrThrow(input) so callers no longer have to distinguish "empty output" from "errors" by inspecting a side property. AssembleOrThrow raises AssemblerException, which carries the collected messages (snapshotted, not the live list). Adds 5 tests. 488 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- memory/MEMORY.md | 58 ++++++++++++++++++++ memory/testkit.md | 71 +++++++++++++++++++++++++ src/Avr8Sharp/Core/Utils/Assembler.cs | 71 +++++++++++++++++++------ tests/Avr8Sharp.Tests/AssemblerTests.cs | 43 ++++++++++++++- 4 files changed, 226 insertions(+), 17 deletions(-) create mode 100644 memory/MEMORY.md create mode 100644 memory/testkit.md diff --git a/memory/MEMORY.md b/memory/MEMORY.md new file mode 100644 index 0000000..3d645bb --- /dev/null +++ b/memory/MEMORY.md @@ -0,0 +1,58 @@ +# AVR8Sharp Project Memory + +## Project Structure +- Solution: `AVR8Sharp.sln` at repo root +- Core lib: `src/AVR8Sharp.Core/` (multi-target: net6, net8, netstandard1.2, netstandard2.0, net10) +- TestKit: `src/Avr8Sharp.TestKit/` (net10, FluentAssertions 6.12.0) +- Tests: `tests/Avr8Sharp.Tests/` (NUnit, net10) +- Demo: `src/Avr8Sharp.Demo/` (net10) + +## TestKit Architecture (created 2026-02-28) +See `memory/testkit.md` for details. + +Key entry point: +```csharp +var sim = AvrTestSimulation.Create() + .WithFrequency(16_000_000) + .WithHex(hexString) // or .WithAsm() / .WithProgram() + .AddGpio(AvrIoPort.PortBConfig, out var portB) + .AddUsart(AvrUsart.Usart0Config, out var serial) + .AddTimer(AvrTimer.Timer0Config); + +sim.RunMilliseconds(500); + +portB.Should().HavePinHigh(5); +serial.Should().Contain("Hello"); +sim.Cpu.Should().HaveRegister(24, 42); +sim.Memory.Should().HaveByteAt(0x200, 0xFF); +``` + +Pre-configured boards (src/Avr8Sharp.TestKit/Boards/): +- ArduinoUnoSimulation — ATmega328P, 16 MHz, PortB/C/D, Timer0/1/2, Serial +- ArduinoMegaSimulation — ATmega2560, 16 MHz, PortA-L, Timer0-4, Serial0/1/2 +- ATtiny85Simulation — ATtiny85, 8 MHz, PortB, Timer0 + +CI/CD workflows (.github/workflows/): +- ci.yml — build + test on every push/PR (GitHub + Gitea compatible) +- publish.yml — pack + push NuGet on tag/main/dispatch; uses secrets: + NUGET_PRIVATE_SOURCE_URL, NUGET_PRIVATE_API_KEY, NUGET_PUBLIC_API_KEY +- build-nugets.yml — deprecated stub (replaced by publish.yml) + +NuGet.Config: XML comments must NOT contain '--' (invalid XML). + +## Key Namespaces +- `Avr8Sharp.TestKit` — AvrTestSimulation, AvrMemoryView, AssertionExtensions +- `Avr8Sharp.TestKit.Probes` — SerialProbe +- `Avr8Sharp.TestKit.Assertions` — AvrCpuAssertions, AvrGpioAssertions, SerialProbeAssertions, AvrMemoryAssertions + +## Core CPU Facts +- `Cpu.Data[0..31]` = general registers R0–R31 +- `Cpu.Data[93..94]` = SP (little-endian via DataView) +- `Cpu.Data[95]` = SREG +- `Cpu.PC` = word address (byte address = PC × 2) +- `Cpu.Cycles` = int (wraps ~134 s at 16 MHz) +- BREAK opcode = 0x9598 + +## Existing Test Utilities +`tests/Avr8Sharp.Tests/Utils.cs` has `TestProgramRunner` and `AsmProgram()` helpers +(these are superseded by TestKit for new tests). diff --git a/memory/testkit.md b/memory/testkit.md new file mode 100644 index 0000000..7da4c68 --- /dev/null +++ b/memory/testkit.md @@ -0,0 +1,71 @@ +# Avr8Sharp.TestKit — Design Notes + +## Files Created +``` +src/Avr8Sharp.TestKit/ +├── Avr8Sharp.TestKit.csproj # FluentAssertions 6.12.0 + Core ref +├── AvrTestSimulation.cs # Fluent builder + execution engine +├── AvrMemoryView.cs # Read-only SRAM wrapper for assertions +├── Probes/ +│ └── SerialProbe.cs # Captures USART TX bytes +├── Assertions/ +│ ├── AvrCpuAssertions.cs # HaveRegister, HavePC, HaveSP, SREG flags +│ ├── SerialProbeAssertions.cs # Contain, StartWith, ContainLine, etc. +│ ├── AvrGpioAssertions.cs # HavePinHigh, HavePinLow, HaveOutputValue +│ └── AvrMemoryAssertions.cs # HaveByteAt, HaveWordAt, HaveBytesAt +└── Extensions/ + └── AssertionExtensions.cs # .Should() for AvrCpu, SerialProbe, AvrIoPort, AvrMemoryView +``` + +## AvrTestSimulation API + +### Configuration (builder methods, all return `this`) +- `.Create(flashSize, sramBytes)` — static factory (default: 0x8000, 8192) +- `.WithFrequency(uint hz)` — default 16 MHz +- `.LoadHex(string)` — Intel HEX (gcc/avra output) +- `.LoadAsm(string)` — inline assembly via AvrAssembler +- `.LoadBytes(byte[])` — raw binary +- `.AddGpio(AvrPortConfig, out AvrIoPort)` +- `.AddUsart(AvrUsartConfig, out SerialProbe)` — auto-captures TX +- `.AddUsart(AvrUsartConfig)` — no capture +- `.AddTimer(AvrTimerConfig)` / `.AddTimer(config, out AvrTimer)` +- `.AddSpi(AvrSpiConfig, out AvrSpi)` +- `.AddTwi(AvrTwiConfig, out AvrTwi)` +- `.AddAdc(AvrAdcConfig, out AvrAdc)` + +### Execution (all return `this`) +- `.RunCycles(long)` — precise cycle count +- `.RunMilliseconds(double)` — simulated time +- `.RunInstructions(int)` — instruction count +- `.RunUntil(Func, maxInstructions=100_000)` +- `.RunToBreak(maxInstructions=100_000)` — stops AT BREAK (0x9598), not executing it +- `.RunToAddress(int byteAddress, maxInstructions=100_000)` + +## Assertion Classes + +### AvrCpuAssertions (`cpu.Should()`) +- `.HaveRegister(index, byte)` — R0–R31 +- `.HavePC(uint)` — word address +- `.HaveSP(ushort)` +- `.HaveCycles(int)` +- `.HaveSreg(byte)` — raw SREG byte +- `.HaveCarryFlag(bool=true)` / `.HaveZeroFlag` / `.HaveNegativeFlag` / `.HaveOverflowFlag` / `.HaveHalfCarryFlag` / `.HaveInterruptsEnabled` + +### SerialProbeAssertions (`serial.Should()`) +- `.Contain(string)` / `.NotContain` / `.StartWith` / `.EndWith` / `.Be` +- `.BeEmpty()` +- `.HaveLineCount(int)` / `.ContainLine(string)` + +### AvrGpioAssertions (`portB.Should()`) +- `.HavePinHigh(int)` / `.HavePinLow(int)` / `.HavePinInput(int)` / `.HavePinInputPullup(int)` +- `.HavePinState(int, PinState)` +- `.HaveOutputValue(byte)` — bitmask of all 8 output pins + +### AvrMemoryAssertions (`sim.Memory.Should()`) +- `.HaveByteAt(address, byte)` / `.HaveWordAt(address, ushort)` / `.HaveWordBEAt` / `.HaveBytesAt(address, byte[])` + +## SerialProbe +- `probe.Text` — all captured output as string +- `probe.Lines` — split on '\n', '\r' trimmed +- `probe.Clear()` — reset buffer +- `probe.InjectByte(byte)` — simulate incoming RX byte (calls `AvrUsart.WriteByte`) diff --git a/src/Avr8Sharp/Core/Utils/Assembler.cs b/src/Avr8Sharp/Core/Utils/Assembler.cs index 502ada4..58cacd6 100644 --- a/src/Avr8Sharp/Core/Utils/Assembler.cs +++ b/src/Avr8Sharp/Core/Utils/Assembler.cs @@ -600,6 +600,30 @@ public byte[] Assemble (string input) return _errors.Count > 0 ? [] : PassTwo(); } + /// + /// Assemble and report success explicitly. Returns false when any error was + /// produced (in which case is empty) and exposes the + /// collected messages, so callers cannot mistake an error for empty output. + /// + public bool TryAssemble (string input, out byte[] bytes, out IReadOnlyList errors) + { + bytes = Assemble(input); + errors = _errors.AsReadOnly(); + return _errors.Count == 0; + } + + /// + /// Assemble, throwing if any error was produced + /// instead of silently returning an empty array. + /// + public byte[] AssembleOrThrow (string input) + { + var bytes = Assemble(input); + if (_errors.Count > 0) + throw new AssemblerException(_errors.ToList()); + return bytes; + } + /// /// Assemble multiple source files together. /// Pass 1: Scan all files for .global exports → build combined symbol table. @@ -846,7 +870,7 @@ private void PassOne (string inputData) } if (dirName == "ENDMACRO" || dirName == "ENDM") { - _errors.Add($"Line {idx}: .endm without .macro"); + _errors.Add($"Line {idx + 1}: .endm without .macro"); continue; } @@ -854,7 +878,7 @@ private void PassOne (string inputData) if (dirName == "INCLUDE") { if (_fileResolver == null) - _errors.Add($"Line {idx}: .include requires a file resolver"); + _errors.Add($"Line {idx + 1}: .include requires a file resolver"); continue; } @@ -883,8 +907,8 @@ private void PassOne (string inputData) if (dirName == "ORG") { int? orgVal = ExpressionEvaluator.TryEvaluate(dirArgs, _symbolTable, byteOffset); - if (orgVal == null) { _errors.Add($"Line {idx}: Cannot evaluate .org expression"); continue; } - if ((orgVal.Value & 1) != 0) { _errors.Add($"Line {idx}: .org value must be even"); continue; } + if (orgVal == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .org expression"); continue; } + if ((orgVal.Value & 1) != 0) { _errors.Add($"Line {idx + 1}: .org value must be even"); continue; } byteOffset = orgVal.Value; continue; } @@ -937,7 +961,7 @@ private void PassOne (string inputData) continue; // Unknown dot-directive: fall through to error - _errors.Add($"Line {idx}: Unknown directive: .{dirName}"); + _errors.Add($"Line {idx + 1}: Unknown directive: .{dirName}"); continue; } @@ -1029,7 +1053,7 @@ private void PassOne (string inputData) _lines.Add(lt); } catch (Exception e) { - _errors.Add ($"Line {idx}: {e.Message}"); + _errors.Add ($"Line {idx + 1}: {e.Message}"); } } @@ -1045,7 +1069,7 @@ private void LoadDeviceSymbols(string deviceName, int lineIdx = -1) if (def == null) { if (lineIdx >= 0) - _errors.Add($"Line {lineIdx}: Unknown device: {deviceName}"); + _errors.Add($"Line {lineIdx + 1}: Unknown device: {deviceName}"); return; } @@ -1113,29 +1137,29 @@ private void ProcessConditional(string directive, string args, int byteOffset, r private void ProcessSymbolDef(string args, int lineIdx, int byteOffset, bool isImmutable) { var parts = args.Split('=', 2); - if (parts.Length != 2) { _errors.Add($"Line {lineIdx}: .equ/.set requires NAME = VALUE"); return; } + if (parts.Length != 2) { _errors.Add($"Line {lineIdx + 1}: .equ/.set requires NAME = VALUE"); return; } var name = parts[0].Trim(); var valStr = parts[1].Trim(); var val = ExpressionEvaluator.TryEvaluate(valStr, _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate expression for '{name}'"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate expression for '{name}'"); return; } try { if (isImmutable) _symbolTable.DefineConst(name, val.Value); else _symbolTable.DefineVar(name, val.Value); } - catch (Exception ex) { _errors.Add($"Line {lineIdx}: {ex.Message}"); } + catch (Exception ex) { _errors.Add($"Line {lineIdx + 1}: {ex.Message}"); } } private void ProcessDefDirective(string args, int lineIdx) { var parts = args.Split('=', 2); - if (parts.Length != 2) { _errors.Add($"Line {lineIdx}: .def requires ALIAS = rN"); return; } + if (parts.Length != 2) { _errors.Add($"Line {lineIdx + 1}: .def requires ALIAS = rN"); return; } var alias = parts[0].Trim(); var regStr = parts[1].Trim(); int n = Encoders.EncoderHelpers.TryParseRegister(regStr.AsSpan()); - if (n < 0) { _errors.Add($"Line {lineIdx}: .def: right side must be a register, got '{regStr}'"); return; } + if (n < 0) { _errors.Add($"Line {lineIdx + 1}: .def: right side must be a register, got '{regStr}'"); return; } try { _symbolTable.DefineRegisterAlias(alias, n); } - catch (Exception ex) { _errors.Add($"Line {lineIdx}: {ex.Message}"); } + catch (Exception ex) { _errors.Add($"Line {lineIdx + 1}: {ex.Message}"); } } // ----------------------------------------------------------------------- @@ -1155,7 +1179,7 @@ private void EmitDataBytes(string args, int lineIdx, ref int byteOffset, string continue; } var val = ExpressionEvaluator.TryEvaluate(p, _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .byte expression: {p}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .byte expression: {p}"); return; } bytes.Add((byte)(val.Value & 0xFF)); } if (bytes.Count > 0) @@ -1173,7 +1197,7 @@ private void EmitDataWords(string args, int lineIdx, ref int byteOffset, string foreach (var part in parts) { var val = ExpressionEvaluator.TryEvaluate(part.Trim(), _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .word expression: {part.Trim()}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .word expression: {part.Trim()}"); return; } wordBytes.Add((byte)(val.Value & 0xFF)); wordBytes.Add((byte)((val.Value >> 8) & 0xFF)); } @@ -1192,7 +1216,7 @@ private void EmitDataDwords(string args, int lineIdx, ref int byteOffset, string foreach (var part in parts) { var val = ExpressionEvaluator.TryEvaluate(part.Trim(), _symbolTable, byteOffset); - if (val == null) { _errors.Add($"Line {lineIdx}: Cannot evaluate .dword expression: {part.Trim()}"); return; } + if (val == null) { _errors.Add($"Line {lineIdx + 1}: Cannot evaluate .dword expression: {part.Trim()}"); return; } dwordBytes.Add((byte)(val.Value & 0xFF)); dwordBytes.Add((byte)((val.Value >> 8) & 0xFF)); dwordBytes.Add((byte)((val.Value >> 16) & 0xFF)); @@ -1629,3 +1653,18 @@ public class LineTable : LineTablePassOne { public new string Bytes { get; set; } } + +/// +/// Thrown by when assembly produced errors. +/// +public class AssemblerException : Exception +{ + /// The individual assembler error messages. + public IReadOnlyList Errors { get; } + + public AssemblerException(IReadOnlyList errors) + : base($"Assembly failed with {errors.Count} error(s):\n " + string.Join("\n ", errors)) + { + Errors = errors; + } +} diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 3e9846b..7eddf02 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -73,7 +73,7 @@ public void Empty_When_Error () Assert.That(result, Is.Empty); Assert.That(assembler.Errors, Has.Count.EqualTo(1)); - Assert.That(assembler.Errors[0], Is.EqualTo("Line 0: Rd out of range: 16<>31")); + Assert.That(assembler.Errors[0], Is.EqualTo("Line 1: Rd out of range: 16<>31")); Assert.That(assembler.Lines, Is.Empty); Assert.That(assembler.Labels, Is.Empty); @@ -920,6 +920,47 @@ public void UnknownDirectiveStillErrors () Assert.That (assembler.Errors, Is.Not.Empty); } + [Test(Description = "Error messages report 1-based line numbers")] + public void ErrorLineNumbersAreOneBased () + { + var assembler = new AvrAssembler (); + assembler.Assemble ("nop\nnop\n.frobnicate 1"); + + Assert.That (assembler.Errors, Has.Count.EqualTo (1)); + Assert.That (assembler.Errors[0], Does.StartWith ("Line 3:")); + } + + [Test(Description = "TryAssemble returns false and surfaces errors on failure")] + public void TryAssemble_Failure () + { + var assembler = new AvrAssembler (); + var ok = assembler.TryAssemble (".frobnicate 1", out var bytes, out var errors); + + Assert.That (ok, Is.False); + Assert.That (bytes, Is.Empty); + Assert.That (errors, Is.Not.Empty); + } + + [Test(Description = "TryAssemble returns true with bytes on success")] + public void TryAssemble_Success () + { + var assembler = new AvrAssembler (); + var ok = assembler.TryAssemble ("ADD r16, r11", out var bytes, out var errors); + + Assert.That (ok, Is.True); + Assert.That (errors, Is.Empty); + Assert.That (Bytes ("0b0d"), Is.EqualTo (bytes)); + } + + [Test(Description = "AssembleOrThrow throws AssemblerException carrying the errors")] + public void AssembleOrThrow_Throws () + { + var assembler = new AvrAssembler (); + var ex = Assert.Throws (() => assembler.AssembleOrThrow (".frobnicate 1")); + + Assert.That (ex!.Errors, Is.Not.Empty); + } + private byte[] Bytes (string hex) { var result = new byte[hex.Length / 2]; From e5377123ac3f1359832f201da422a580c112cef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:28:38 -0600 Subject: [PATCH 3/6] feat(assembler): add ATmega2560 device definition The simulator models the ATmega2560 (Arduino Mega) but `.device` only knew ATmega328P and ATtiny85. Adds a full ATmega2560 register map (178 symbols: ports A-L, USART0-3, timers 0-5, SPI, TWI, ADC, EEPROM, external/pin-change interrupts, clock/power control) plus RAMEND/FLASHEND/E2END. Addresses extracted verbatim from avr-libc / iomxx0_1.h, using the same convention as the existing definitions: low I/O (data < 0x60) stored as the I/O address, extended I/O (>= 0x60) as the data-space address. Extended-I/O accesses (sts UDR3/PORTL, lds TWBR) were verified byte-identical to avr-as -mmcu=atmega2560. Adds 4 tests. 492 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs | 221 ++++++++++++++++++ .../AssemblerNewFeaturesTests.cs | 40 ++++ 2 files changed, 261 insertions(+) diff --git a/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs b/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs index 8615063..5ff5e08 100644 --- a/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs +++ b/src/Avr8Sharp/Core/Utils/DeviceDefinitions.cs @@ -27,6 +27,7 @@ public static class DeviceDefinitions static DeviceDefinitions() { Register(ATmega328P()); + Register(ATmega2560()); Register(ATtiny85()); } @@ -179,6 +180,226 @@ static DeviceDefinitions() } }; + // ----------------------------------------------------------------------- + // ATmega2560 (Arduino Mega 2560) + // Datasheet: DS40002211A — Microchip ATmega640/1280/1281/2560/2561 + // Register addresses extracted from avr-libc (iomxx0_1.h). + // Low I/O (data < 0x60) stored as I/O address; extended I/O (>= 0x60) as + // data-space address — same convention as the other device definitions. + // ----------------------------------------------------------------------- + private static DeviceDefinition ATmega2560() => new() + { + Name = "ATmega2560", + FlashSize = 0x40000, // 256 KB + SramSize = 8192, + EepromSize = 4096, + Symbols = new(StringComparer.OrdinalIgnoreCase) + { + // ── Memory limits ── + ["RAMEND"] = 0x21FF, + ["FLASHEND"] = 0x3FFFF, + ["E2END"] = 0x0FFF, + // ── Port A-G (low I/O) ── + ["PINA"] = 0x00, + ["DDRA"] = 0x01, + ["PORTA"] = 0x02, + ["PINB"] = 0x03, + ["DDRB"] = 0x04, + ["PORTB"] = 0x05, + ["PINC"] = 0x06, + ["DDRC"] = 0x07, + ["PORTC"] = 0x08, + ["PIND"] = 0x09, + ["DDRD"] = 0x0A, + ["PORTD"] = 0x0B, + ["PINE"] = 0x0C, + ["DDRE"] = 0x0D, + ["PORTE"] = 0x0E, + ["PINF"] = 0x0F, + ["DDRF"] = 0x10, + ["PORTF"] = 0x11, + ["PING"] = 0x12, + ["DDRG"] = 0x13, + ["PORTG"] = 0x14, + // ── Port H-L (extended I/O) ── + ["PINH"] = 0x100, + ["DDRH"] = 0x101, + ["PORTH"] = 0x102, + ["PINJ"] = 0x103, + ["DDRJ"] = 0x104, + ["PORTJ"] = 0x105, + ["PINK"] = 0x106, + ["DDRK"] = 0x107, + ["PORTK"] = 0x108, + ["PINL"] = 0x109, + ["DDRL"] = 0x10A, + ["PORTL"] = 0x10B, + // ── Status / stack / RAMPZ / EIND ── + ["SREG"] = 0x3F, + ["SPL"] = 0x3D, + ["SPH"] = 0x3E, + ["RAMPZ"] = 0x3B, + ["EIND"] = 0x3C, + // ── EEPROM ── + ["EECR"] = 0x1F, + ["EEDR"] = 0x20, + ["EEARL"] = 0x21, + ["EEARH"] = 0x22, + // ── General-purpose I/O regs ── + ["GPIOR0"] = 0x1E, + ["GPIOR1"] = 0x2A, + ["GPIOR2"] = 0x2B, + // ── SPI ── + ["SPCR"] = 0x2C, + ["SPSR"] = 0x2D, + ["SPDR"] = 0x2E, + // ── TWI ── + ["TWBR"] = 0xB8, + ["TWSR"] = 0xB9, + ["TWAR"] = 0xBA, + ["TWDR"] = 0xBB, + ["TWCR"] = 0xBC, + ["TWAMR"] = 0xBD, + // ── ADC ── + ["ADCL"] = 0x78, + ["ADCH"] = 0x79, + ["ADCSRA"] = 0x7A, + ["ADCSRB"] = 0x7B, + ["ADMUX"] = 0x7C, + ["DIDR0"] = 0x7E, + ["DIDR1"] = 0x7F, + ["DIDR2"] = 0x7D, + // ── Timer/Counter 0 (8-bit) ── + ["TIFR0"] = 0x15, + ["TCCR0A"] = 0x24, + ["TCCR0B"] = 0x25, + ["TCNT0"] = 0x26, + ["OCR0A"] = 0x27, + ["OCR0B"] = 0x28, + ["TIMSK0"] = 0x6E, + // ── Timer/Counter 1 (16-bit) ── + ["TIFR1"] = 0x16, + ["TCCR1A"] = 0x80, + ["TCCR1B"] = 0x81, + ["TCCR1C"] = 0x82, + ["TCNT1L"] = 0x84, + ["TCNT1H"] = 0x85, + ["ICR1L"] = 0x86, + ["ICR1H"] = 0x87, + ["OCR1AL"] = 0x88, + ["OCR1AH"] = 0x89, + ["OCR1BL"] = 0x8A, + ["OCR1BH"] = 0x8B, + ["OCR1CL"] = 0x8C, + ["OCR1CH"] = 0x8D, + ["TIMSK1"] = 0x6F, + // ── Timer/Counter 2 (8-bit async) ── + ["TIFR2"] = 0x17, + ["TCCR2A"] = 0xB0, + ["TCCR2B"] = 0xB1, + ["TCNT2"] = 0xB2, + ["OCR2A"] = 0xB3, + ["OCR2B"] = 0xB4, + ["ASSR"] = 0xB6, + ["TIMSK2"] = 0x70, + // ── Timer/Counter 3 (16-bit) ── + ["TIFR3"] = 0x18, + ["TCCR3A"] = 0x90, + ["TCCR3B"] = 0x91, + ["TCCR3C"] = 0x92, + ["TCNT3L"] = 0x94, + ["TCNT3H"] = 0x95, + ["ICR3L"] = 0x96, + ["ICR3H"] = 0x97, + ["OCR3AL"] = 0x98, + ["OCR3AH"] = 0x99, + ["OCR3BL"] = 0x9A, + ["OCR3BH"] = 0x9B, + ["OCR3CL"] = 0x9C, + ["OCR3CH"] = 0x9D, + ["TIMSK3"] = 0x71, + // ── Timer/Counter 4 (16-bit) ── + ["TIFR4"] = 0x19, + ["TCCR4A"] = 0xA0, + ["TCCR4B"] = 0xA1, + ["TCCR4C"] = 0xA2, + ["TCNT4L"] = 0xA4, + ["TCNT4H"] = 0xA5, + ["ICR4L"] = 0xA6, + ["ICR4H"] = 0xA7, + ["OCR4AL"] = 0xA8, + ["OCR4AH"] = 0xA9, + ["OCR4BL"] = 0xAA, + ["OCR4BH"] = 0xAB, + ["OCR4CL"] = 0xAC, + ["OCR4CH"] = 0xAD, + ["TIMSK4"] = 0x72, + // ── Timer/Counter 5 (16-bit) ── + ["TIFR5"] = 0x1A, + ["TCCR5A"] = 0x120, + ["TCCR5B"] = 0x121, + ["TCCR5C"] = 0x122, + ["TCNT5L"] = 0x124, + ["TCNT5H"] = 0x125, + ["ICR5L"] = 0x126, + ["ICR5H"] = 0x127, + ["OCR5AL"] = 0x128, + ["OCR5AH"] = 0x129, + ["OCR5BL"] = 0x12A, + ["OCR5BH"] = 0x12B, + ["OCR5CL"] = 0x12C, + ["OCR5CH"] = 0x12D, + ["TIMSK5"] = 0x73, + // ── USART0 ── + ["UCSR0A"] = 0xC0, + ["UCSR0B"] = 0xC1, + ["UCSR0C"] = 0xC2, + ["UBRR0L"] = 0xC4, + ["UBRR0H"] = 0xC5, + ["UDR0"] = 0xC6, + // ── USART1 ── + ["UCSR1A"] = 0xC8, + ["UCSR1B"] = 0xC9, + ["UCSR1C"] = 0xCA, + ["UBRR1L"] = 0xCC, + ["UBRR1H"] = 0xCD, + ["UDR1"] = 0xCE, + // ── USART2 ── + ["UCSR2A"] = 0xD0, + ["UCSR2B"] = 0xD1, + ["UCSR2C"] = 0xD2, + ["UBRR2L"] = 0xD4, + ["UBRR2H"] = 0xD5, + ["UDR2"] = 0xD6, + // ── USART3 ── + ["UCSR3A"] = 0x130, + ["UCSR3B"] = 0x131, + ["UCSR3C"] = 0x132, + ["UBRR3L"] = 0x134, + ["UBRR3H"] = 0x135, + ["UDR3"] = 0x136, + // ── External / pin-change interrupts ── + ["EIMSK"] = 0x1D, + ["EIFR"] = 0x1C, + ["EICRA"] = 0x69, + ["EICRB"] = 0x6A, + ["PCICR"] = 0x68, + ["PCIFR"] = 0x1B, + ["PCMSK0"] = 0x6B, + ["PCMSK1"] = 0x6C, + ["PCMSK2"] = 0x6D, + // ── Misc control ── + ["MCUSR"] = 0x34, + ["MCUCR"] = 0x35, + ["SMCR"] = 0x33, + ["PRR0"] = 0x64, + ["PRR1"] = 0x65, + ["CLKPR"] = 0x61, + ["WDTCSR"] = 0x60, + ["OSCCAL"] = 0x66, + } + }; + // ----------------------------------------------------------------------- // ATtiny85 (Digispark / Trinket) // Datasheet: DS40002311A — Microchip ATtiny25/45/85 diff --git a/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs b/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs index 0f969cd..e2acf54 100644 --- a/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerNewFeaturesTests.cs @@ -422,6 +422,46 @@ public void Device_CaseInsensitive() Assert.That(result, Is.EqualTo(Bytes("05b9"))); } +[Test] +public void Device_ATmega2560_ExtendedIO_Usart3() +{ +// UDR3 = data-space 0x136 (extended I/O) — verified against avr-as -mmcu=atmega2560 +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("sts UDR3, r16"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("00933601"))); // STS 0x136, r16 +} + +[Test] +public void Device_ATmega2560_PortL() +{ +// PORTL = data-space 0x10B (port H-L block) — verified against avr-as +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("sts PORTL, r16"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("00930b01"))); // STS 0x10B, r16 +} + +[Test] +public void Device_ATmega2560_Twi_LowIO() +{ +// TWBR = data-space 0xB8 (extended I/O) — verified against avr-as +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("lds r17, TWBR"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("1091b800"))); // LDS r17, 0xB8 +} + +[Test] +public void Device_ATmega2560_RAMEND() +{ +// RAMEND = 0x21FF (8 KB SRAM); lo8 = 0xFF +var asm = new AvrAssembler(deviceName: "ATmega2560"); +var result = asm.Assemble("ldi r16, lo8(RAMEND)"); +Assert.That(asm.Errors, Is.Empty); +Assert.That(result, Is.EqualTo(Bytes("0fef"))); // LDI r16, 0xFF +} + // --- Multi-file assembly (P2) --- [Test] public void Global_Extern_DirectivesAccepted() From b52eba1cc0a7c6cfad0f364175e600de1ffa793c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:30:41 -0600 Subject: [PATCH 4/6] feat(assembler): support '.' location-counter arithmetic (.+N / .-N) The tokenizer now emits a bare '.' as a current-PC token (reusing the Dollar kind the expression evaluator already resolves), so relative branches accept `.+N` / `.-N` operands. Verified byte-identical to avr-as: `rjmp .+2` => 01c0, `rjmp .-2` => ffcf, `rcall .+4` => 02d0. Dotted directives/labels (.L2) are unaffected (still tokenized as before). Adds a test. 493 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Avr8Sharp/Core/Utils/Tokenizer.cs | 9 +++++++++ tests/Avr8Sharp.Tests/AssemblerTests.cs | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Avr8Sharp/Core/Utils/Tokenizer.cs b/src/Avr8Sharp/Core/Utils/Tokenizer.cs index ee18c1b..24278e2 100644 --- a/src/Avr8Sharp/Core/Utils/Tokenizer.cs +++ b/src/Avr8Sharp/Core/Utils/Tokenizer.cs @@ -152,6 +152,15 @@ public static List Tokenize(string line) continue; } + // Bare '.' = current location counter (used in expressions like ".+4"). + // Reuses the Dollar token kind, which the evaluator resolves to the current PC. + if (c == '.') + { + tokens.Add(new Token(TokenKind.Dollar, ".", col)); + i++; + continue; + } + // Identifiers, mnemonics, registers if (char.IsLetter(c) || c == '_') { diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 7eddf02..06599ec 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -839,6 +839,14 @@ public void RJMP_Dot () Assert.That (Bytes ("00c0"), Is.EqualTo (assembler.Assemble ("rjmp ."))); } + [Test(Description = "'.' location-counter arithmetic (.+N / .-N) matches avr-as")] + public void DotArithmetic () + { + Assert.That (Bytes ("0000" + "01c0"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrjmp .+2"))); + Assert.That (Bytes ("0000" + "ffcf"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrjmp .-2"))); + Assert.That (Bytes ("0000" + "02d0"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrcall .+4"))); + } + // ----------------------------------------------------------------------- // avr-gcc / avr-as front-end compatibility // ----------------------------------------------------------------------- From 0f1cda01e58aba8a3c54b260e78c47e0a0a023b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:32:53 -0600 Subject: [PATCH 5/6] feat(assembler): add .rept/.endr repeat blocks GNU `.rept ... .endr` blocks are recorded like a macro body and injected times. Composes with the existing \name / @n macro parameter substitution (verified: a .rept around a \reg macro expands correctly and matches avr-as for plain bodies). A stray .endr is reported as an error. Note: \name and @n macro parameters were already supported; this adds the repeat construct. Adds 3 tests. 496 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Avr8Sharp/Core/Utils/Assembler.cs | 41 +++++++++++++++++++++++++ tests/Avr8Sharp.Tests/AssemblerTests.cs | 30 ++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/Avr8Sharp/Core/Utils/Assembler.cs b/src/Avr8Sharp/Core/Utils/Assembler.cs index 58cacd6..8d868e2 100644 --- a/src/Avr8Sharp/Core/Utils/Assembler.cs +++ b/src/Avr8Sharp/Core/Utils/Assembler.cs @@ -777,6 +777,10 @@ private void PassOne (string inputData) List? macroParams = null; List? macroBody = null; + // .rept block recording state + List? reptBody = null; + int reptCount = 0; + // Use a list + index so we can inject macro-expanded lines var lineList = allLines; int lineCount = lineList.Count; @@ -813,6 +817,28 @@ private void PassOne (string inputData) continue; } + // ---------------------------------------------------------------- + // .rept block recording: collect the body, then inject it reptCount + // times after .endr (GNU repeat blocks). + // ---------------------------------------------------------------- + if (reptBody != null) + { + if (keyword is "ENDR" or "ENDREPT") + { + var injected = new List(reptBody.Count * Math.Max(reptCount, 0)); + for (int r = 0; r < reptCount; r++) injected.AddRange(reptBody); + lineList = lineList.Take(idx + 1).Concat(injected).Concat(lineList.Skip(idx + 1)).ToList(); + lineCount = lineList.Count; + reptBody = null; + reptCount = 0; + } + else + { + reptBody.Add(rawLine); + } + continue; + } + // ---------------------------------------------------------------- // Conditional assembly directives (must process even in false branch) // ---------------------------------------------------------------- @@ -874,6 +900,21 @@ private void PassOne (string inputData) continue; } + // Repeat block start: .rept ... .endr + if (dirName == "REPT") + { + int? n = ExpressionEvaluator.TryEvaluate(dirArgs, _symbolTable, byteOffset); + if (n == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .rept count"); continue; } + reptCount = Math.Max(n.Value, 0); + reptBody = new List(); + continue; + } + if (dirName == "ENDR" || dirName == "ENDREPT") + { + _errors.Add($"Line {idx + 1}: .endr without .rept"); + continue; + } + // Include (should have been expanded, but handle gracefully) if (dirName == "INCLUDE") { diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 06599ec..9e6d47c 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -847,6 +847,36 @@ public void DotArithmetic () Assert.That (Bytes ("0000" + "02d0"), Is.EqualTo (new AvrAssembler ().Assemble ("nop\nrcall .+4"))); } + [Test(Description = ".rept N / .endr repeats the block N times (matches avr-as)")] + public void ReptBlock () + { + var assembler = new AvrAssembler (); + var result = assembler.Assemble (".rept 3\n nop\n.endr"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("000000000000"), Is.EqualTo (result)); // 3 NOPs (2 bytes each) + } + + [Test(Description = ".rept composes with a \\name-parameterised macro")] + public void ReptWithMacro () + { + var assembler = new AvrAssembler (); + var result = assembler.Assemble (".macro twice reg\n inc \\reg\n inc \\reg\n.endm\n.rept 2\n twice r16\n.endr"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("03950395" + "03950395"), Is.EqualTo (result)); // 4x INC r16 + } + + [Test(Description = ".endr without .rept is an error")] + public void OrphanEndr () + { + var assembler = new AvrAssembler (); + assembler.Assemble ("nop\n.endr"); + + Assert.That (assembler.Errors, Has.Count.EqualTo (1)); + Assert.That (assembler.Errors[0], Does.Contain (".endr without .rept")); + } + // ----------------------------------------------------------------------- // avr-gcc / avr-as front-end compatibility // ----------------------------------------------------------------------- From e148ed9fbbafe0c78875b653c81352cf4d105cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Montiel=20Cardona?= Date: Wed, 24 Jun 2026 02:46:06 -0600 Subject: [PATCH 6/6] feat(assembler): data segment (.data/.bss/.comm/.lcomm) + forward-ref fixes Adds a minimal SRAM data segment so avr-gcc programs that reference global data assemble: - .dseg/.data/.bss/.section .data|.bss switch to a data segment; .cseg/.text switch back. Labels in the data segment, and .comm/.lcomm NAME,SIZE, get sequential SRAM addresses from a simple allocator (base 0x100). Data directives (.byte/.word/...) and .skip/.space/.zero reserve space without emitting code. lds/sts then resolve these symbols. Addresses are self-consistent but are NOT avr-gcc's linker-assigned ones. To support data symbols defined after their use (avr-gcc emits .data last), LDS/STS now defer to pass two like CALL/JMP. Doing so surfaced two latent bugs, both fixed here: - ElementSize concatenated a resolved 4-byte KeyValuePair into a string, so a deferred 4-byte instruction as the LAST line was misread as 2 bytes (affected CALL/JMP too). Now keeps the pair. - _currentSymbolTable was nulled at the end of pass one, so forward-referenced address *expressions* (`lds rN, sym+1`) could not resolve in pass two. It is now restored at the start of pass two. Validated: all six avr-gcc -Os -S sample programs (including a global-pointer one) now assemble. Adds 4 tests. 500 pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Avr8Sharp/Core/Utils/Assembler.cs | 114 +++++++++++++++++++++--- tests/Avr8Sharp.Tests/AssemblerTests.cs | 43 +++++++++ 2 files changed, 144 insertions(+), 13 deletions(-) diff --git a/src/Avr8Sharp/Core/Utils/Assembler.cs b/src/Avr8Sharp/Core/Utils/Assembler.cs index 8d868e2..99b15b5 100644 --- a/src/Avr8Sharp/Core/Utils/Assembler.cs +++ b/src/Avr8Sharp/Core/Utils/Assembler.cs @@ -318,8 +318,12 @@ public partial class AvrAssembler r |= ((k & 0xf0) << 4) | (k & 0xf); return ZeroPad (r); }}, - { "LDS", (a, b, _, _) => { - var k = ConstValue(b, 0, 65535); + { "LDS", (a, b, byteLoc, labels) => { + var k = ConstOrLabel(b, labels); + if (k == int.MinValue) { + return new Func, object> ((l) => OpTable?["LDS"](a, b, byteLoc, l) ?? new KeyValuePair(string.Empty, string.Empty)); + } + k = ConstValue(k, 0, 65535); var r = 0x9000 | DestRIndex(a); return new KeyValuePair(ZeroPad(r), ZeroPad(k)); }}, @@ -523,8 +527,12 @@ public partial class AvrAssembler var r = 0x0200 | DestRIndex(b) | StldYzQ(a); return ZeroPad (r); }}, - { "STS", (a, b, _, _) => { - var k = ConstValue(a, 0, 65535); + { "STS", (a, b, byteLoc, labels) => { + var k = ConstOrLabel(a, labels); + if (k == int.MinValue) { + return new Func, object> ((l) => OpTable?["STS"](a, b, byteLoc, l) ?? new KeyValuePair(string.Empty, string.Empty)); + } + k = ConstValue(k, 0, 65535); var r = 0x9200 | DestRIndex(b); return new KeyValuePair(ZeroPad(r), ZeroPad(k)); }}, @@ -764,6 +772,15 @@ private void PassOne (string inputData) _currentSymbolTable = _symbolTable; int byteOffset = 0; + + // Data segment (.data/.bss/.dseg) state. Symbols defined here are assigned + // sequential SRAM addresses by a simple allocator (no linker), starting at + // 0x100 — the SRAM base of the ATmega family. This lets lds/sts reference + // data symbols; the addresses are self-consistent but are NOT avr-gcc's + // linker-assigned addresses. + bool inDataSegment = false; + int dataOffset = 0x100; + _labels.Clear(); _errors.Clear(); _lines.Clear(); @@ -858,8 +875,9 @@ private void PassOne (string inputData) // ---------------------------------------------------------------- if (parsed.Label != null) { - _labels[parsed.Label] = byteOffset; - _symbolTable.Set(parsed.Label, byteOffset); + int labelAddr = inDataSegment ? dataOffset : byteOffset; + _labels[parsed.Label] = labelAddr; + _symbolTable.Set(parsed.Label, labelAddr); } // ---------------------------------------------------------------- @@ -923,9 +941,46 @@ private void PassOne (string inputData) continue; } - // Segment directives (simple) - if (dirName == "CSEG" || dirName == "DSEG" || dirName == "ESEG") - continue; // segment switching not fully implemented; cseg is default + // Segment switching. .dseg/.data/.bss select the data segment (SRAM + // allocator); .cseg/.text return to code. .section picks by its argument. + if (dirName == "DSEG" || dirName == "DATA" || dirName == "BSS") + { + inDataSegment = true; + continue; + } + if (dirName == "CSEG" || dirName == "TEXT" || dirName == "ESEG") + { + inDataSegment = false; // eseg (EEPROM) is not modelled; treat as code-ish no-op + continue; + } + if (dirName == "SECTION") + { + var sec = dirArgs.TrimStart().Split(new[] { ' ', '\t', ',' }, 2)[0]; + inDataSegment = sec.StartsWith(".data", StringComparison.OrdinalIgnoreCase) + || sec.StartsWith(".bss", StringComparison.OrdinalIgnoreCase); + continue; + } + + // Reserve space: .comm/.lcomm NAME,SIZE and .skip/.space/.zero SIZE. + if (dirName == "COMM" || dirName == "LCOMM") + { + var parts = dirArgs.Split(','); + if (parts.Length < 2) { _errors.Add($"Line {idx + 1}: .{dirName.ToLowerInvariant()} requires NAME, SIZE"); continue; } + var symName = parts[0].Trim(); + int? size = ExpressionEvaluator.TryEvaluate(parts[1], _symbolTable, dataOffset); + if (size == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .{dirName.ToLowerInvariant()} size"); continue; } + _symbolTable.Set(symName, dataOffset); + _labels[symName] = dataOffset; + dataOffset += Math.Max(size.Value, 0); + continue; + } + if (inDataSegment && (dirName == "SKIP" || dirName == "SPACE" || dirName == "ZERO")) + { + int? size = ExpressionEvaluator.TryEvaluate(dirArgs.Split(',')[0], _symbolTable, dataOffset); + if (size == null) { _errors.Add($"Line {idx + 1}: Cannot evaluate .{dirName.ToLowerInvariant()} size"); continue; } + dataOffset += Math.Max(size.Value, 0); + continue; + } // Symbol definitions if (dirName == "EQU") @@ -954,6 +1009,15 @@ private void PassOne (string inputData) continue; } + // In the data segment, data directives only reserve address space + // (advance the SRAM allocator) — no bytes are emitted into the code image. + if (inDataSegment && dirName is "BYTE" or "DB" or "WORD" or "DW" or "DWORD" + or "ASCII" or "ASCIZ" or "STRING") + { + dataOffset += DataReserveSize(dirName, dirArgs); + continue; + } + // Data directives — emit raw bytes into line table if (dirName == "BYTE" || dirName == "DB") { @@ -995,7 +1059,7 @@ private void PassOne (string inputData) // 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" + if (dirName is "FILE" 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") @@ -1203,6 +1267,25 @@ private void ProcessDefDirective(string args, int lineIdx) catch (Exception ex) { _errors.Add($"Line {lineIdx + 1}: {ex.Message}"); } } + // ----------------------------------------------------------------------- + // Number of bytes a data directive reserves (used in the data segment, where + // the directive advances the SRAM allocator without emitting code bytes). + // String length is approximate for escape sequences — adequate for reservation. + // ----------------------------------------------------------------------- + private int DataReserveSize(string dirName, string args) + { + var parts = SplitDirectiveArgs(args); + if (dirName is "ASCII" or "ASCIZ" or "STRING") + { + int n = 0; + foreach (var p in parts) + n += p.Trim().Trim('"', '\'').Length; + return dirName == "ASCII" ? n : n + parts.Count; // one NUL per string for asciz/string + } + int unit = dirName is "WORD" or "DW" ? 2 : dirName == "DWORD" ? 4 : 1; + return parts.Count * unit; + } + // ----------------------------------------------------------------------- // Data-emission helpers // ----------------------------------------------------------------------- @@ -1351,8 +1434,13 @@ private static List SplitDirectiveArgs(string args) private byte [] PassTwo () { _errors.Clear(); - - if (_lines.Count == 0) + + // Restore the symbol table for deferred closures: forward-referenced + // expressions (e.g. `lds rN, sym+1`) resolve through ConstOrLabel's + // expression evaluator, which reads _currentSymbolTable here in pass two. + _currentSymbolTable = _symbolTable; + + if (_lines.Count == 0) return []; var lastElement = _lines[_lines.Count - 1]; @@ -1412,7 +1500,7 @@ private int ElementSize (ref LineTablePassOne lt) return s2.Length / 2; } if (res is KeyValuePair p) { - lt.Bytes = p.Key + p.Value; + lt.Bytes = p; // keep the 4-byte pair; concatenating would be misread as a 2-byte string return 4; } } diff --git a/tests/Avr8Sharp.Tests/AssemblerTests.cs b/tests/Avr8Sharp.Tests/AssemblerTests.cs index 9e6d47c..e69ba36 100644 --- a/tests/Avr8Sharp.Tests/AssemblerTests.cs +++ b/tests/Avr8Sharp.Tests/AssemblerTests.cs @@ -877,6 +877,49 @@ public void OrphanEndr () Assert.That (assembler.Errors[0], Does.Contain (".endr without .rept")); } + [Test(Description = "Data-segment labels get sequential SRAM addresses (lds/sts resolve)")] + public void DataSegment_Labels () + { + var assembler = new AvrAssembler (); + // buf=0x100, flag=0x101; lds r16,buf / lds r17,flag + var result = assembler.Assemble (".data\nbuf: .byte 1\nflag: .byte 1\n.text\nlds r16, buf\nlds r17, flag"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("00910001" + "10910101"), Is.EqualTo (result)); + } + + [Test(Description = ".lcomm reserves SRAM and forward-referenced data symbols resolve in lds/sts")] + public void DataSegment_LcommAndForwardRef () + { + var assembler = new AvrAssembler (); + // data segment placed after code (avr-gcc style); counter resolves to 0x100 + var result = assembler.Assemble ("lds r16, counter\nsts counter, r16\n.data\ncounter: .byte 1"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("00910001" + "00930001"), Is.EqualTo (result)); + } + + [Test(Description = "Forward-referenced address expression (sym+1) resolves in lds/sts (pass two)")] + public void DataSegment_ForwardExpression () + { + var assembler = new AvrAssembler (); + // p = 0x100 via .comm placed after use; p+1 must resolve to 0x101 + var result = assembler.Assemble ("lds r17, p+1\n.comm p, 2"); + + Assert.That (assembler.Errors, Is.Empty); + Assert.That (Bytes ("10910101"), Is.EqualTo (result)); // LDS r17, 0x101 + } + + [Test(Description = "A deferred 4-byte instruction as the last line resolves correctly")] + public void Deferred4ByteAsLastLine () + { + // Regression: ElementSize concatenated the resolved pair into a 2-byte string. + var call = new AvrAssembler (); + Assert.That (Bytes ("0e940300" + "0000"), Is.EqualTo (call.Assemble ("call end\nnop\nend:"))); + var lds = new AvrAssembler (); + Assert.That (Bytes ("00910001"), Is.EqualTo (lds.Assemble ("lds r16, x\n.data\nx: .byte 1"))); + } + // ----------------------------------------------------------------------- // avr-gcc / avr-as front-end compatibility // -----------------------------------------------------------------------