diff --git a/src/Chip8.OctoAsm/Chip8.OctoAsm.csproj b/src/Chip8.OctoAsm/Chip8.OctoAsm.csproj
new file mode 100644
index 0000000..1ded6a1
--- /dev/null
+++ b/src/Chip8.OctoAsm/Chip8.OctoAsm.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net6.0
+ disable
+ enable
+ Chip8.OctoAsm
+ Chip8.OctoAsm
+ 10
+
+
+
+
+
+
+
diff --git a/src/Chip8.OctoAsm/OctoDiagnostic.cs b/src/Chip8.OctoAsm/OctoDiagnostic.cs
new file mode 100644
index 0000000..9cc8951
--- /dev/null
+++ b/src/Chip8.OctoAsm/OctoDiagnostic.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+namespace Chip8.OctoAsm;
+
+public enum OctoSeverity
+{
+ Error,
+ Warning,
+}
+
+public readonly struct OctoDiagnostic
+{
+ public OctoDiagnostic(OctoSeverity severity, string message, SourcePosition position)
+ {
+ this.severity = severity;
+ this.message = message;
+ this.position = position;
+ }
+
+ public OctoSeverity severity { get; }
+ public string message { get; }
+ public SourcePosition position { get; }
+
+ public int line => position.line;
+ public int column => position.column;
+
+ public override string ToString() =>
+ severity.ToString().ToLowerInvariant() + " at " + position + ": " + message;
+}
+
+public sealed class OctoCompileException : Exception
+{
+ public OctoCompileException(OctoDiagnostic diagnostic)
+ : this(new[] { diagnostic })
+ {
+ }
+
+ public OctoCompileException(IReadOnlyList diagnostics)
+ : base(describe(diagnostics))
+ {
+ this.diagnostics = diagnostics;
+ }
+
+ public IReadOnlyList diagnostics { get; }
+
+ public OctoDiagnostic diagnostic => diagnostics[0];
+
+ static string describe(IReadOnlyList diagnostics)
+ {
+ if (diagnostics.Count == 1)
+ {
+ return diagnostics[0].ToString();
+ }
+ return diagnostics.Count + " Octo compile errors; first: " + diagnostics[0];
+ }
+}
diff --git a/src/Chip8.OctoAsm/OctoToken.cs b/src/Chip8.OctoAsm/OctoToken.cs
new file mode 100644
index 0000000..990eb13
--- /dev/null
+++ b/src/Chip8.OctoAsm/OctoToken.cs
@@ -0,0 +1,51 @@
+namespace Chip8.OctoAsm;
+
+public enum OctoTokenKind
+{
+ Word,
+ Number,
+ OpenBrace,
+ CloseBrace,
+ OpenParen,
+ CloseParen,
+ EndOfFile,
+}
+
+public readonly struct SourcePosition
+{
+ public SourcePosition(int line, int column)
+ {
+ this.line = line;
+ this.column = column;
+ }
+
+ public int line { get; }
+ public int column { get; }
+
+ public override string ToString() => "line " + line + ", col " + column;
+}
+
+public readonly struct OctoToken
+{
+ public OctoToken(OctoTokenKind kind, string text, int value, SourcePosition position)
+ {
+ this.kind = kind;
+ this.text = text;
+ this.value = value;
+ this.position = position;
+ }
+
+ public OctoTokenKind kind { get; }
+ public string text { get; }
+ public int value { get; }
+ public SourcePosition position { get; }
+
+ public bool isWord(string word) => kind == OctoTokenKind.Word && text == word;
+
+ public override string ToString() => kind switch
+ {
+ OctoTokenKind.EndOfFile => "",
+ OctoTokenKind.Number => "number(" + value + ")",
+ _ => "'" + text + "'",
+ };
+}