create x86 playground lessons #94

Closed
opened 2026-05-10 03:53:16 +00:00 by spikerj · 1 comment
Owner

x86 Playground: convert into a lesson-based playground (sandbox + lessons), teaching GNU as / Fasm / NASM

Background

The x86 playground at /tools/(tools:x86-playground) is currently sandbox-only. It already has:

  • A Blink WASM emulator (BlinkService, Blink) wired through PlaygroundStoreService.
  • A unified menubar with Compile / Run / Step / Continue / Stop, an assembler picker (GNU as / Fasm / NASM), an ELF loader, a Share dialog, and the standard lib-tool-file-menu Save / Download / Load to/from Browser/Device entries.
  • Six bundled examples (SNIPPETS in services/example-snippets.ts): Syscall (GNU as), Syscall (Fasm), Syscall (NASM), Functions (GNU as), Functions (Fasm), Functions (NASM).
  • Three statically-bundled assemblers (ASSEMBLERS in services/assembler-config.ts):
    • GNU as (gnu-as.2.43.50.elf + gnu-ld.2.43.50.elf, Intel syntax flavour)
    • Fasm (fasm.1.73.32.elf, single binary — assembler == linker)
    • NASM (nasm.3.00.elf + gnu-ld.2.43.50.elf)
  • Granular activity tracking via tools.x86 in KnowledgeDomainRegistry.cs (compile, run, step, starti, continue, stop, select-assembler, load-example, reset-view, share, load-elf, save/load/download File I/O).

It does not have lessons. Every other "language playground" we ship (tools.csharp-playground, tools.python-playground, tools.javascript-playground, tools.regex) has a structured lesson curriculum that pairs a sidebar of progressive challenges with the same editor surface. This ticket adds the same shape to x86, with one twist: the user must be able to learn each of the three assembler dialects (GNU as, Fasm, NASM) so the curriculum has to span three flavours in parallel rather than one.

Goals

  1. Turn the existing x86 playground into a dual-mode UI (sandbox | lesson), mirroring <app-language-runner> and reg-ex.component patterns.
  2. Author an x86 curriculum (~30-60 lessons across foundational chapters) that teaches the same conceptual progression in all three assembler flavours, so a student can learn x86-64 in any one — or compare the three.
  3. Grade lessons entirely in the browser through the existing Blink WASM emulator + assembler binaries, exactly like Regex grading is browser-only. Server hosts the catalog, attempt token, and TestPlan; the SPA assembles, runs, and validates locally.
  4. Reuse existing platform plumbing — LessonCatalogService, ProgressSyncService, lesson-platform types, LessonStrategyBase, the offline IDB queue — so this is a curriculum + grader feature, not a rewrite of platform infrastructure.

Non-goals

  • No server-side x86 execution. Blink ships in the SPA; we do not stand up a server worker.
  • No new assembler dialects beyond GNU as / Fasm / NASM in this ticket.
  • No "step-debugger" lesson kind in v1 — lessons grade the final program output / exit code / register state after run-to-completion. (Step-trace lessons are listed under "Future work".)
  • No LessonShape analogue. x86 source is wholesale, not method-body wrapped.

Non-negotiables

  • The curriculum must respect the cardinal pedagogy rule (.cursor/rules/curriculum-pedagogy.mdc, regex flavour in regex-curriculum-pedagogy.mdc): a lesson must never require an instruction, directive, syntax form, or syscall that hasn't been introduced in a prerequisite lesson. Each lesson declares an IntroducedInstructions / IntroducedDirectives / IntroducedSyscalls set; a smoke test must enforce that every reference solution's tokens are a subset of the prerequisite closure.
  • Three dialects in lockstep. Where a concept exists in all three (most of Tiers 1-5), every lesson ships three reference solutions (GNU/Fasm/NASM) and the student picks which dialect to write. The test plan accepts any of the three — it grades the program's behaviour, not the source. Where a concept is dialect-specific (e.g. Fasm's macro format directive), the lesson is scoped to that one dialect and the sidebar badge says so.
  • No regressions for sandbox users. Sandbox mode keeps every existing affordance: the assembler picker, ELF loader, snippet menu, Share dialog, File menu, drag-drop overlay. The mode toggle defaults to sandbox for unauthenticated users (mirrors C#/Python/JS).

Phase 1 — Backend: lesson contract for x86

1.1 New runtime + categories

Extend ILessonAttemptStrategy.cs:

public enum LessonGradingRuntime
{
    RoslynCSharp = 0,
    CPython = 1,
    NodeJavaScript = 2,
    RegexBrowser = 3,
    /// <summary>
    /// Browser-first grading via the Blink WASM emulator. The student's submission
    /// is x86-64 assembly source for one of the three supported flavours (GNU as,
    /// Fasm, NASM). The SPA picks the matching ASSEMBLERS entry, calls
    /// BlinkService.compile() + run(), and compares the resulting stdout / exit
    /// code / register snapshot against the lesson's TestPlan. Stays out of the
    /// server worker tier — Blink already ships in the bundle.
    /// </summary>
    BlinkX86 = 4,
}

Extend LessonCategory.cs with the x86 chapter set (one per tier):

Enum value Tier Topic
X86Welcome 0 Tutorial-only intro: what is assembly, what x86-64 is, syscalls vs instructions, dialect overview.
X86Registers 1 mov, immediates, register-to-register, the 16 GP registers, sub-registers (rax/eax/ax/al).
X86ArithmeticLogic 2 add, sub, inc, dec, xor, and, or, neg, not, RFLAGS basics.
X86MemoryAddressing 3 lea, [base+index*scale+disp], dword/qword sizing, .data vs .bss (or Fasm equivalents).
X86ControlFlow 4 cmp, test, jmp, je/jne/jl/jg family, labels, simple loops.
X86Stack 5 push/pop, rsp discipline, stack frames, alignment (16-byte before call).
X86CallingConvention 6 System V AMD64 ABI: rdi rsi rdx rcx r8 r9 integer args, rax return, callee-saved set, call/ret.
X86Syscalls 7 Linux syscall ABI: rax = syscall number, syscall instruction, read/write/exit, errno semantics.
X86Strings 8 Pointer arithmetic on .asciz/db strings, manual strlen, rep movsb, rep stosb.
X86DialectQuirks 9 Dialect-specific idioms each labelled to its flavour: Fasm format ELF64 executable 3 + entry $; NASM section .text + global _start; GNU .intel_syntax noprefix + .global _start.

1.2 Lesson number band

Reserve 5xxxx for x86 (parallels Python 2xxxx, JavaScript 3xxxx, Regex 4xxxx):

Range Chapter
50000-50099 Tier 0 Welcome / dialect picker tutorials
50100-50199 Tier 1 Registers
50200-50299 Tier 2 Arithmetic & logic
50300-50399 Tier 3 Memory & addressing
50400-50499 Tier 4 Control flow
50500-50599 Tier 5 Stack
50600-50699 Tier 6 Calling convention
50700-50799 Tier 7 Syscalls
50800-50899 Tier 8 Strings
50900-50999 Tier 9 Dialect-specific quirks

DisplayNumber is a hand-assigned 1..N sequence across the whole x86 track (mirrors regex). Default LessonStrategyBase derivation DisplayNumber ?? LessonNumber is wrong here for the same reason regex needed an override — set explicitly per lesson.

1.3 X86LessonStrategyBase

New Curriculum/X86/X86LessonStrategyBase.cs, modelled on RegexLessonStrategyBase:

  • Inherits LessonStrategyBase. Subclasses override BuildX86Plan(rng) (Challenge) or BuildTutorial(rng) (Tutorial).

  • The plan is serialised into LessonAttemptState.TestCode as JSON; GetLessonAttemptQueryHandler already knows how to copy BrowserGradingState for browser-graded runtimes — extend the if (runtime == RegexBrowser) branch to also include BlinkX86.

  • Plan record:

    public sealed record X86LessonPlan(
        X86LessonAssertion Assertion,
        // Optional starter sources per dialect — null means "use the empty
        // dialect-default skeleton". Lessons that pre-seed a known scaffold
        // (e.g. "_start: <fill in here>") populate just the dialects they want.
        string? GnuStarter,
        string? FasmStarter,
        string? NasmStarter,
        // Subset of {"GNU","FASM","NASM"} — which dialects this lesson accepts.
        // Lessons in X86DialectQuirks frequently set this to a single dialect.
        IReadOnlyList<string> AllowedDialects,
        // Tokens this lesson introduces (instructions, directives, registers,
        // syscalls). The cardinal-rule smoke test enforces that the union of
        // every prereq's set is a superset of every accepted reference
        // solution's tokens.
        X86LessonIntroduced Introduced);
    
    public abstract record X86LessonAssertion;
    
    // Most common — student program writes to stdout; we compare bytes.
    public sealed record StdoutEqualsAssertion(string Expected) : X86LessonAssertion;
    
    // Program must exit with rdi (the exit-syscall arg) equal to ExpectedCode.
    public sealed record ExitCodeEqualsAssertion(int ExpectedCode) : X86LessonAssertion;
    
    // After run-to-completion, the named registers must hold the expected values.
    // Used for register-arithmetic lessons that don't write to stdout (Tiers 1-2).
    public sealed record RegisterStateAssertion(
        IReadOnlyDictionary<string, long> Expected) : X86LessonAssertion;
    
    // Composite — used when a lesson wants both "your program exits 0" AND
    // "your program prints exactly X". Evaluated as AND.
    public sealed record AllOfAssertion(
        IReadOnlyList<X86LessonAssertion> Children) : X86LessonAssertion;
    
  • X86LessonIntroduced is (IReadOnlyList<string> Instructions, IReadOnlyList<string> Directives, IReadOnlyList<string> Registers, IReadOnlyList<string> Syscalls). Tokens are case-insensitive; canonicalise to lower-case at construction.

1.4 Lesson layout on disk

SpikerSoft.Business/Domain/Lessons/Curriculum/X86/
├── X86LessonStrategyBase.cs
├── X86LessonPlan.cs
├── Tier00_Welcome/
│   ├── Lesson50000_WhatIsAssembly.cs            (Tutorial)
│   ├── Lesson50001_X8664Briefly.cs               (Tutorial)
│   ├── Lesson50002_DialectPicker.cs              (Tutorial — explains GNU vs Fasm vs NASM)
│   └── Lesson50003_FirstCompileAndRun.cs         (Tutorial — show a hello world)
├── Tier01_Registers/
│   ├── Lesson50100_MovImmediate.cs               (set rax = 42)
│   ├── Lesson50101_MovBetweenRegisters.cs
│   ├── Lesson50102_SubRegisters.cs               (rax / eax / ax / al)
│   └── ...
├── Tier02_ArithmeticLogic/
├── Tier03_MemoryAddressing/
├── Tier04_ControlFlow/
├── Tier05_Stack/
├── Tier06_CallingConvention/
├── Tier07_Syscalls/
│   ├── Lesson50700_ExitWithCode.cs               (exit(7))
│   ├── Lesson50701_HelloWorld.cs                 (write+exit) ← rewrite of the existing snippet, now graded
│   └── ...
├── Tier08_Strings/
└── Tier09_DialectQuirks/
    ├── Lesson50900_FasmFormatDirective.cs        (AllowedDialects = ["FASM"])
    ├── Lesson50901_NasmSectionGlobal.cs          (AllowedDialects = ["NASM"])
    └── Lesson50902_GnuIntelSyntax.cs             (AllowedDialects = ["GNU"])

Target initial scope: ~40-50 lessons. The full table can be filled in via a follow-up PR per chapter. The first deliverable is Tiers 0-2 (~12 lessons) so the UI / grader path is exercised end-to-end before the rest of the curriculum lands.

1.5 Reference solutions + smoke tests

Mirror the existing pattern (ReferenceSolutions.cs, RegexReferenceSolutions.cs). New file SpikerSoft.Tests.Unit/Domain/Lessons/Curriculum/X86ReferenceSolutions.cs with one entry per (lesson, dialect) pair. Smoke tests:

  • X86Challenge_ReferenceSolutionsAssemble_<NUMBER>_<DIALECT> — invoke a real as/fasm/nasm (host-installed, not Blink — keep the dotnet test self-contained) and verify the produced ELF runs to completion against the assertion. For CI portability, fall back to Blink in a Node harness (Blink already ships as a self-contained WASM module), but assembler binaries are large — see "Open questions" below.
  • X86Curriculum_IntroducedTokensRespectPrerequisites — for every accepted reference solution, tokenise the source and verify every token is in the lesson's prereq closure. (Tokeniser only needs to recognise instructions/directives — a permissive \b[a-z._]+\b lex over the source plus a static keyword set gets us 95% there.)
  • X86Curriculum_AllowedDialectsHaveReferenceSolutions — every dialect in AllowedDialects has a matching reference entry.

1.6 Catalog hydration

LessonCatalogHydrationService already iterates over LessonStrategyRegistry.All and projects metadata into Mongo — no change required. LessonCatalogService.listByLanguage needs a new accepted value: "x86" (or "X86" — match the existing casing convention, which uses "C#" | "Python" | "JavaScript" | "Regex"; suggest "x86" for naming consistency with the route).

Update the type union in lesson-catalog.service.ts and the deprecated per-language helpers comment block.

1.7 Activity tracking registry

Extend tools.x86 in KnowledgeDomainRegistry.cs with the lesson-mode actions used in every other playground (these come from the shared BuildPlaygroundActions-style action set):

  • lesson-open (with lessonId metadata)
  • lesson-submit
  • lesson-pass
  • lesson-fail
  • lesson-complete
  • tutorial-complete
  • view-lessons-menu
  • select-dialect-for-lesson (metadata: {from, to})
  • start-tour / complete-tour already exist on other playgrounds; add here too.

A DomainAlias entry should map tools.x86-playgroundtools.x86 so any future client emission using the long form still aggregates correctly (the registry already has the alias mechanism — see DomainAliases block).


Phase 2 — Frontend: dual-mode UI

2.1 Mode toggle + sidebar

Mirror the regex playground:

  • Add a mode = signal<"sandbox" | "lesson">("sandbox") to AssemblyPlaygroundComponent (or a new X86PlaygroundShellComponent if the file gets too large).
  • Add a left sidebar (collapsible on handset, fixed on desktop) listing chapters → lessons. Reuse the existing regex-lessons-menu SCSS partials if reasonable; otherwise factor a shared lesson-sidebar component.
  • Each lesson row renders the title, displayNumber, completion badge, and a per-lesson dialect chip ("GNU / Fasm / NASM" or just one if scoped).
  • Sandbox-only chrome (the standalone snippet picker, the "Reset View" button) collapses in lesson mode — exactly how C#/Python/JS hide the sandbox-level switch in lesson mode.

2.2 X86LessonRunnerService

New libraries/features/dev-tools-x86-playground/src/lib/services/x86-lesson-runner.service.ts. Modelled on RegexLessonRunnerService:

  • Holds catalog signal + progress signal; resolves an attempt via LessonCatalogService.getAttempt(lessonNumber) (existing endpoint).
  • Owns the dialect-pick state for the current lesson (defaults to FASM to match DEFAULT_SNIPPET_ID unless the lesson restricts AllowedDialects).
  • On loadLesson(n), calls BlinkService.setMode(...), sets editorContent from the dialect-matched starter, clears assemblerErrors / terminal, and emits tools.x86 / lesson-open with { lessonId, displayNumber, dialect }.
  • Exposes submit() which:
    1. Calls BlinkService.compile() (fail-fast if assemblerErrors is non-empty — surface as LessonTestResult { passed: false, error: "Assembler error: <first message>" }).
    2. Awaits BlinkService.run() to completion (use emulatorState transitions; need a small firstValueFrom-style helper since the store is signal-based — settle on the first transition into "finished" or "crashed", with a 5s wall-clock guard).
    3. Snapshots terminalText, stopReason, register state (via a new Blink.snapshotRegisters() helper if not already exposed).
    4. Hands (plan, snapshot) to a new X86LessonGraderService.grade(plan, snapshot) which evaluates each X86LessonAssertion and returns LessonTestResult[].
    5. POSTs the result to the existing offline-aware completion endpoint (same path Regex uses) so progress and skill awards stay consistent.

2.3 X86LessonGraderService

New x86-lesson-grader.service.ts. Pure function — no DI'd services, no HTTP. Mirrors RegexLessonGraderService in shape:

  • grade(plan: X86LessonPlan, snapshot: { stdout: string; exitCode: number | null; registers: Record<string, bigint>; }): LessonTestResult[]
  • One LessonTestResult per leaf assertion (AllOfAssertion flattens). PASS/FAIL strings echo the existing PASS: / FAIL: / ERROR: prefix discipline so any downstream parser stays valid.
  • Includes a small formatRegisterDiff helper so failed register assertions render Expected rax = 0x2A, got rax = 0x00.

2.4 Lesson pane

Re-use RegexLessonPaneComponent is not the right call (regex-specific affordances). Instead, factor a lightweight <app-x86-lesson-pane> that renders:

  • Title, description, instructions list, hints accordion (same shape as Regex pane).
  • A dialect picker (mat-button-toggle-group — values constrained to the lesson's AllowedDialects). Switching dialect mid-lesson swaps the editor source from the matching starter and emits select-dialect-for-lesson.
  • Submit button (disabled while emulator is running).
  • Test results list reusing the existing test-result chip styles.

Tutorials reuse the existing <app-tutorial-pane> with no x86-specific code.

2.5 Bundle + lazy-load discipline

  • Lessons UI must lazy-load with the existing route. The Blink WASM module already lazy-loads on first sandbox mount; lesson mode must not eagerly load it on route nav unless the user actually clicks a lesson.
  • Avoid pulling all 40-50 lesson plan blobs into the bundle. Plans live server-side; the client fetches per-attempt the same way every other playground does.

Phase 3 — Activity tracking + CI

  • Update the staff-only /admin/activity-coverage page to include x86 lesson actions in the registry.
  • Run node scripts/check-activity-tracking-drift.cjs and clear any drift introduced by Phase 2 emissions.
  • Add an nx test spec for X86LessonRunnerService (catalog load, lesson open emits the right activity event, dialect switch updates editor + emits event, submit pipeline calls grader and surfaces results).
  • Add an nx test spec for X86LessonGraderService covering each assertion variant.

Acceptance criteria

  • LessonGradingRuntime.BlinkX86 = 4 exists and GetLessonAttemptQueryHandler includes the runtime in the BrowserGradingState projection branch.
  • X86LessonStrategyBase and X86LessonPlan (with StdoutEqualsAssertion, ExitCodeEqualsAssertion, RegisterStateAssertion, AllOfAssertion) are defined and unit-tested.
  • At least Tiers 0-2 of the curriculum (~12 lessons) ship with reference solutions for every AllowedDialects entry, and X86Curriculum_IntroducedTokensRespectPrerequisites is green for every reference solution.
  • LessonCategory includes the ten new x86 categories.
  • LessonCatalogService.listByLanguage accepts "x86".
  • The frontend has a sandboxlesson mode toggle in the menubar; sandbox mode is unchanged for existing users.
  • Lesson mode renders a chapter sidebar driven by the catalog, opens lessons, switches the assembler dialect, runs Blink, grades results, and reports completion through the same offline-aware path Regex uses.
  • All four assertion variants render PASS/FAIL rows in the test results panel, with the same chip styles every other playground uses.
  • Lesson mode emits tools.x86 / lesson-open / lesson-submit / lesson-pass / lesson-fail / lesson-complete / tutorial-complete / select-dialect-for-lesson events; drift script is green.
  • No bundle regression beyond the established x86-playground baseline (record current size in the PR and call out the delta).
  • The Tree of Knowledge surfaces x86 lesson actions distinctly from sandbox actions (no aggregation under compile/run).

References (code)

Backend:

  • SpikerSoft.Business/Domain/Lessons/ILessonAttemptStrategy.csLessonGradingRuntime, LessonMetadata, LessonAttempt, LessonAttemptState.
  • SpikerSoft.Business/Domain/Lessons/LessonStrategyBase.cs — base-class pattern subclasses follow.
  • SpikerSoft.Business/Domain/Lessons/LessonCategory.cs — categories enum to extend.
  • SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/RegexLessonStrategyBase.cs — closest analogue (browser-graded runtime, plan-via-JSON channel).
  • SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/RegexLessonPlan.cs — record-shape pattern for X86LessonPlan.
  • SpikerSoft.Business/Domain/Lessons/Queries/GetLessonAttempt/GetLessonAttemptQueryHandler.cs — extend the RegexBrowser projection branch to include BlinkX86.
  • SpikerSoft.Business/Domain/Activity/KnowledgeDomainRegistry.cstools.x86 definition (line ~195) to extend with lesson actions.
  • SpikerSoft.Tests.Unit/Domain/Lessons/Curriculum/RegexReferenceSolutions.cs — pattern for X86ReferenceSolutions.cs.

Frontend:

  • libraries/features/dev-tools-x86-playground/src/lib/assembly-playground.component.ts — current shell to add mode-switching to.
  • libraries/features/dev-tools-x86-playground/src/lib/services/blink.service.ts — emulator façade; extend with snapshotRegisters() and a "wait for finished/crashed" helper.
  • libraries/features/dev-tools-x86-playground/src/lib/services/assembler-config.ts + example-snippets.ts — existing dialect / snippet wiring; lesson starters reuse this AssemblersKey.
  • libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-runner.service.ts — closest runner analogue.
  • libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-grader.service.ts — closest grader analogue (browser-only, pure function).
  • libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-pane.component.ts — lesson-pane shape to mirror.
  • libraries/features/dev-tools-reg-ex/src/lib/reg-ex.component.tsmode toggle, sidebar, lesson-load → editor-source pipeline.
  • libraries/platform/lesson-catalog/src/lib/lesson-catalog.service.ts — extend the language union to include "x86".
  • libraries/shared/lesson-platform/src/lib/lesson-types.ts — shared types; add an "x86" language literal to anywhere that constrains it.

Pedagogy rules:

  • .cursor/rules/curriculum-pedagogy.mdc
  • .cursor/rules/regex-curriculum-pedagogy.mdc

Open questions / decisions to settle in the PR

  1. CI assembler binaries. dotnet test runs the smoke test for "every reference solution assembles + runs against the assertion". We can either (a) install as / nasm / fasm on the CI image, or (b) drive Blink + the ELF assembler binaries from a Node harness inside dotnet test (heavy, but self-contained — Blink is already in the SPA bundle and could be vendored into the test project). Recommend (b) only if (a) bloats CI image size meaningfully; otherwise (a) is the simpler path.
  2. AllowedDialects default. Should an unset list mean "all three" or "must be specified explicitly"? Lean toward explicit — every lesson author thinks about this — but accept either with a code-review nudge.
  3. Register snapshot scope. Blink already exposes register state to its UI panels. Confirm there's a clean "give me a Record<string, bigint> of GP regs" API or add one; do not let the grader poke at internal Blink state.
  4. Linker flag in the test plan. Some lessons may want to assert exit code without running through _start boilerplate. Keep _start as the only entry contract for v1; revisit if a lesson would genuinely benefit from a freestanding entry point.
  5. Tutorial dialect coverage. Tutorials in Tier 0 have to introduce all three dialects without overwhelming the student. Suggest one tutorial per dialect plus one shared "what is x86" tutorial, then default Challenge lessons to "any dialect accepted" until Tier 9 (X86DialectQuirks).

Future work (not in this ticket)

  • Step-debugger lesson kind ("set rax = 42 by stepping through the program; the grader checks the register state at each int3 breakpoint").
  • TypeScript-grader-in-browser for inline disassembly puzzles ("given this hex, what does it disassemble to?").
  • ARM64 / RISC-V playground tracks following the same shape.
  • "Dialect translator" lesson kind: write the program in one dialect, the grader assembles it in all three to verify equivalence.
# x86 Playground: convert into a lesson-based playground (sandbox + lessons), teaching GNU as / Fasm / NASM ## Background The x86 playground at `/tools/(tools:x86-playground)` is currently **sandbox-only**. It already has: - A Blink WASM emulator (`BlinkService`, `Blink`) wired through `PlaygroundStoreService`. - A unified menubar with Compile / Run / Step / Continue / Stop, an assembler picker (GNU as / Fasm / NASM), an ELF loader, a Share dialog, and the standard `lib-tool-file-menu` Save / Download / Load to/from Browser/Device entries. - Six bundled examples (`SNIPPETS` in `services/example-snippets.ts`): `Syscall (GNU as)`, `Syscall (Fasm)`, `Syscall (NASM)`, `Functions (GNU as)`, `Functions (Fasm)`, `Functions (NASM)`. - Three statically-bundled assemblers (`ASSEMBLERS` in `services/assembler-config.ts`): - `GNU as` (`gnu-as.2.43.50.elf` + `gnu-ld.2.43.50.elf`, Intel syntax flavour) - `Fasm` (`fasm.1.73.32.elf`, single binary — assembler == linker) - `NASM` (`nasm.3.00.elf` + `gnu-ld.2.43.50.elf`) - Granular activity tracking via `tools.x86` in `KnowledgeDomainRegistry.cs` (compile, run, step, starti, continue, stop, select-assembler, load-example, reset-view, share, load-elf, save/load/download File I/O). It does **not** have lessons. Every other "language playground" we ship (`tools.csharp-playground`, `tools.python-playground`, `tools.javascript-playground`, `tools.regex`) has a structured lesson curriculum that pairs a sidebar of progressive challenges with the same editor surface. This ticket adds the same shape to x86, with one twist: **the user must be able to learn each of the three assembler dialects (GNU as, Fasm, NASM)** so the curriculum has to span three flavours in parallel rather than one. ## Goals 1. Turn the existing x86 playground into a **dual-mode** UI (`sandbox` | `lesson`), mirroring `<app-language-runner>` and `reg-ex.component` patterns. 2. Author an x86 curriculum (~30-60 lessons across foundational chapters) that teaches the **same conceptual progression in all three assembler flavours**, so a student can learn x86-64 in any one — or compare the three. 3. Grade lessons **entirely in the browser** through the existing Blink WASM emulator + assembler binaries, exactly like Regex grading is browser-only. Server hosts the catalog, attempt token, and TestPlan; the SPA assembles, runs, and validates locally. 4. Reuse existing platform plumbing — `LessonCatalogService`, `ProgressSyncService`, `lesson-platform` types, `LessonStrategyBase`, the offline IDB queue — so this is a curriculum + grader feature, not a rewrite of platform infrastructure. ## Non-goals - No server-side x86 execution. Blink ships in the SPA; we do not stand up a server worker. - No new assembler dialects beyond GNU as / Fasm / NASM in this ticket. - No "step-debugger" lesson kind in v1 — lessons grade the **final program output / exit code / register state after run-to-completion**. (Step-trace lessons are listed under "Future work".) - No `LessonShape` analogue. x86 source is wholesale, not method-body wrapped. ## Non-negotiables - **The curriculum must respect the cardinal pedagogy rule** (`.cursor/rules/curriculum-pedagogy.mdc`, regex flavour in `regex-curriculum-pedagogy.mdc`): **a lesson must never require an instruction, directive, syntax form, or syscall that hasn't been introduced in a prerequisite lesson.** Each lesson declares an `IntroducedInstructions` / `IntroducedDirectives` / `IntroducedSyscalls` set; a smoke test must enforce that every reference solution's tokens are a subset of the prerequisite closure. - **Three dialects in lockstep.** Where a concept exists in all three (most of Tiers 1-5), every lesson ships **three reference solutions** (GNU/Fasm/NASM) and the student picks which dialect to write. The test plan accepts any of the three — it grades the program's behaviour, not the source. Where a concept is dialect-specific (e.g. Fasm's macro `format` directive), the lesson is scoped to that one dialect and the sidebar badge says so. - **No regressions for sandbox users.** Sandbox mode keeps every existing affordance: the assembler picker, ELF loader, snippet menu, Share dialog, File menu, drag-drop overlay. The mode toggle defaults to `sandbox` for unauthenticated users (mirrors C#/Python/JS). --- ## Phase 1 — Backend: lesson contract for x86 ### 1.1 New runtime + categories Extend `ILessonAttemptStrategy.cs`: ```csharp public enum LessonGradingRuntime { RoslynCSharp = 0, CPython = 1, NodeJavaScript = 2, RegexBrowser = 3, /// <summary> /// Browser-first grading via the Blink WASM emulator. The student's submission /// is x86-64 assembly source for one of the three supported flavours (GNU as, /// Fasm, NASM). The SPA picks the matching ASSEMBLERS entry, calls /// BlinkService.compile() + run(), and compares the resulting stdout / exit /// code / register snapshot against the lesson's TestPlan. Stays out of the /// server worker tier — Blink already ships in the bundle. /// </summary> BlinkX86 = 4, } ``` Extend `LessonCategory.cs` with the x86 chapter set (one per tier): | Enum value | Tier | Topic | |---|---|---| | `X86Welcome` | 0 | Tutorial-only intro: what is assembly, what x86-64 is, syscalls vs instructions, dialect overview. | | `X86Registers` | 1 | `mov`, immediates, register-to-register, the 16 GP registers, sub-registers (`rax`/`eax`/`ax`/`al`). | | `X86ArithmeticLogic` | 2 | `add`, `sub`, `inc`, `dec`, `xor`, `and`, `or`, `neg`, `not`, RFLAGS basics. | | `X86MemoryAddressing` | 3 | `lea`, `[base+index*scale+disp]`, dword/qword sizing, `.data` vs `.bss` (or Fasm equivalents). | | `X86ControlFlow` | 4 | `cmp`, `test`, `jmp`, `je`/`jne`/`jl`/`jg` family, labels, simple loops. | | `X86Stack` | 5 | `push`/`pop`, `rsp` discipline, stack frames, alignment (16-byte before `call`). | | `X86CallingConvention` | 6 | System V AMD64 ABI: `rdi rsi rdx rcx r8 r9` integer args, `rax` return, callee-saved set, `call`/`ret`. | | `X86Syscalls` | 7 | Linux syscall ABI: `rax` = syscall number, `syscall` instruction, `read`/`write`/`exit`, errno semantics. | | `X86Strings` | 8 | Pointer arithmetic on `.asciz`/`db` strings, manual `strlen`, `rep movsb`, `rep stosb`. | | `X86DialectQuirks` | 9 | Dialect-specific idioms each labelled to its flavour: Fasm `format ELF64 executable 3` + `entry $`; NASM `section .text` + `global _start`; GNU `.intel_syntax noprefix` + `.global _start`. | ### 1.2 Lesson number band Reserve **5xxxx** for x86 (parallels Python 2xxxx, JavaScript 3xxxx, Regex 4xxxx): | Range | Chapter | |---|---| | 50000-50099 | Tier 0 Welcome / dialect picker tutorials | | 50100-50199 | Tier 1 Registers | | 50200-50299 | Tier 2 Arithmetic & logic | | 50300-50399 | Tier 3 Memory & addressing | | 50400-50499 | Tier 4 Control flow | | 50500-50599 | Tier 5 Stack | | 50600-50699 | Tier 6 Calling convention | | 50700-50799 | Tier 7 Syscalls | | 50800-50899 | Tier 8 Strings | | 50900-50999 | Tier 9 Dialect-specific quirks | `DisplayNumber` is a hand-assigned 1..N sequence across the whole x86 track (mirrors regex). Default `LessonStrategyBase` derivation `DisplayNumber ?? LessonNumber` is wrong here for the same reason regex needed an override — set explicitly per lesson. ### 1.3 X86LessonStrategyBase New `Curriculum/X86/X86LessonStrategyBase.cs`, modelled on `RegexLessonStrategyBase`: - Inherits `LessonStrategyBase`. Subclasses override `BuildX86Plan(rng)` (Challenge) or `BuildTutorial(rng)` (Tutorial). - The plan is serialised into `LessonAttemptState.TestCode` as JSON; `GetLessonAttemptQueryHandler` already knows how to copy `BrowserGradingState` for browser-graded runtimes — extend the `if (runtime == RegexBrowser)` branch to also include `BlinkX86`. - Plan record: ```csharp public sealed record X86LessonPlan( X86LessonAssertion Assertion, // Optional starter sources per dialect — null means "use the empty // dialect-default skeleton". Lessons that pre-seed a known scaffold // (e.g. "_start: <fill in here>") populate just the dialects they want. string? GnuStarter, string? FasmStarter, string? NasmStarter, // Subset of {"GNU","FASM","NASM"} — which dialects this lesson accepts. // Lessons in X86DialectQuirks frequently set this to a single dialect. IReadOnlyList<string> AllowedDialects, // Tokens this lesson introduces (instructions, directives, registers, // syscalls). The cardinal-rule smoke test enforces that the union of // every prereq's set is a superset of every accepted reference // solution's tokens. X86LessonIntroduced Introduced); public abstract record X86LessonAssertion; // Most common — student program writes to stdout; we compare bytes. public sealed record StdoutEqualsAssertion(string Expected) : X86LessonAssertion; // Program must exit with rdi (the exit-syscall arg) equal to ExpectedCode. public sealed record ExitCodeEqualsAssertion(int ExpectedCode) : X86LessonAssertion; // After run-to-completion, the named registers must hold the expected values. // Used for register-arithmetic lessons that don't write to stdout (Tiers 1-2). public sealed record RegisterStateAssertion( IReadOnlyDictionary<string, long> Expected) : X86LessonAssertion; // Composite — used when a lesson wants both "your program exits 0" AND // "your program prints exactly X". Evaluated as AND. public sealed record AllOfAssertion( IReadOnlyList<X86LessonAssertion> Children) : X86LessonAssertion; ``` - `X86LessonIntroduced` is `(IReadOnlyList<string> Instructions, IReadOnlyList<string> Directives, IReadOnlyList<string> Registers, IReadOnlyList<string> Syscalls)`. Tokens are case-insensitive; canonicalise to lower-case at construction. ### 1.4 Lesson layout on disk ``` SpikerSoft.Business/Domain/Lessons/Curriculum/X86/ ├── X86LessonStrategyBase.cs ├── X86LessonPlan.cs ├── Tier00_Welcome/ │ ├── Lesson50000_WhatIsAssembly.cs (Tutorial) │ ├── Lesson50001_X8664Briefly.cs (Tutorial) │ ├── Lesson50002_DialectPicker.cs (Tutorial — explains GNU vs Fasm vs NASM) │ └── Lesson50003_FirstCompileAndRun.cs (Tutorial — show a hello world) ├── Tier01_Registers/ │ ├── Lesson50100_MovImmediate.cs (set rax = 42) │ ├── Lesson50101_MovBetweenRegisters.cs │ ├── Lesson50102_SubRegisters.cs (rax / eax / ax / al) │ └── ... ├── Tier02_ArithmeticLogic/ ├── Tier03_MemoryAddressing/ ├── Tier04_ControlFlow/ ├── Tier05_Stack/ ├── Tier06_CallingConvention/ ├── Tier07_Syscalls/ │ ├── Lesson50700_ExitWithCode.cs (exit(7)) │ ├── Lesson50701_HelloWorld.cs (write+exit) ← rewrite of the existing snippet, now graded │ └── ... ├── Tier08_Strings/ └── Tier09_DialectQuirks/ ├── Lesson50900_FasmFormatDirective.cs (AllowedDialects = ["FASM"]) ├── Lesson50901_NasmSectionGlobal.cs (AllowedDialects = ["NASM"]) └── Lesson50902_GnuIntelSyntax.cs (AllowedDialects = ["GNU"]) ``` Target initial scope: ~40-50 lessons. The full table can be filled in via a follow-up PR per chapter. The first deliverable is Tiers 0-2 (~12 lessons) so the UI / grader path is exercised end-to-end before the rest of the curriculum lands. ### 1.5 Reference solutions + smoke tests Mirror the existing pattern (`ReferenceSolutions.cs`, `RegexReferenceSolutions.cs`). New file `SpikerSoft.Tests.Unit/Domain/Lessons/Curriculum/X86ReferenceSolutions.cs` with one entry per (lesson, dialect) pair. Smoke tests: - `X86Challenge_ReferenceSolutionsAssemble_<NUMBER>_<DIALECT>` — invoke a real `as`/`fasm`/`nasm` (host-installed, not Blink — keep the dotnet test self-contained) and verify the produced ELF runs to completion against the assertion. For CI portability, fall back to **Blink in a Node harness** (Blink already ships as a self-contained WASM module), but assembler binaries are large — see "Open questions" below. - `X86Curriculum_IntroducedTokensRespectPrerequisites` — for every accepted reference solution, tokenise the source and verify every token is in the lesson's prereq closure. (Tokeniser only needs to recognise instructions/directives — a permissive `\b[a-z._]+\b` lex over the source plus a static keyword set gets us 95% there.) - `X86Curriculum_AllowedDialectsHaveReferenceSolutions` — every dialect in `AllowedDialects` has a matching reference entry. ### 1.6 Catalog hydration `LessonCatalogHydrationService` already iterates over `LessonStrategyRegistry.All` and projects metadata into Mongo — no change required. `LessonCatalogService.listByLanguage` needs a new accepted value: `"x86"` (or `"X86"` — match the existing casing convention, which uses `"C#" | "Python" | "JavaScript" | "Regex"`; suggest `"x86"` for naming consistency with the route). Update the type union in `lesson-catalog.service.ts` and the deprecated per-language helpers comment block. ### 1.7 Activity tracking registry Extend `tools.x86` in `KnowledgeDomainRegistry.cs` with the lesson-mode actions used in every other playground (these come from the shared `BuildPlaygroundActions`-style action set): - `lesson-open` (with `lessonId` metadata) - `lesson-submit` - `lesson-pass` - `lesson-fail` - `lesson-complete` - `tutorial-complete` - `view-lessons-menu` - `select-dialect-for-lesson` (metadata: `{from, to}`) - `start-tour` / `complete-tour` already exist on other playgrounds; add here too. A `DomainAlias` entry should map `tools.x86-playground` → `tools.x86` so any future client emission using the long form still aggregates correctly (the registry already has the alias mechanism — see `DomainAliases` block). --- ## Phase 2 — Frontend: dual-mode UI ### 2.1 Mode toggle + sidebar Mirror the regex playground: - Add a `mode = signal<"sandbox" | "lesson">("sandbox")` to `AssemblyPlaygroundComponent` (or a new `X86PlaygroundShellComponent` if the file gets too large). - Add a left sidebar (collapsible on handset, fixed on desktop) listing chapters → lessons. Reuse the existing `regex-lessons-menu` SCSS partials if reasonable; otherwise factor a shared `lesson-sidebar` component. - Each lesson row renders the title, `displayNumber`, completion badge, and a per-lesson dialect chip ("GNU / Fasm / NASM" or just one if scoped). - Sandbox-only chrome (the standalone snippet picker, the "Reset View" button) collapses in lesson mode — exactly how C#/Python/JS hide the sandbox-level switch in lesson mode. ### 2.2 X86LessonRunnerService New `libraries/features/dev-tools-x86-playground/src/lib/services/x86-lesson-runner.service.ts`. Modelled on `RegexLessonRunnerService`: - Holds catalog signal + progress signal; resolves an attempt via `LessonCatalogService.getAttempt(lessonNumber)` (existing endpoint). - Owns the dialect-pick state for the current lesson (defaults to `FASM` to match `DEFAULT_SNIPPET_ID` unless the lesson restricts `AllowedDialects`). - On `loadLesson(n)`, calls `BlinkService.setMode(...)`, sets `editorContent` from the dialect-matched starter, clears assemblerErrors / terminal, and emits `tools.x86 / lesson-open` with `{ lessonId, displayNumber, dialect }`. - Exposes `submit()` which: 1. Calls `BlinkService.compile()` (fail-fast if `assemblerErrors` is non-empty — surface as `LessonTestResult { passed: false, error: "Assembler error: <first message>" }`). 2. Awaits `BlinkService.run()` to completion (use `emulatorState` transitions; need a small `firstValueFrom`-style helper since the store is signal-based — settle on the first transition into `"finished"` or `"crashed"`, with a 5s wall-clock guard). 3. Snapshots `terminalText`, `stopReason`, register state (via a new `Blink.snapshotRegisters()` helper if not already exposed). 4. Hands `(plan, snapshot)` to a new `X86LessonGraderService.grade(plan, snapshot)` which evaluates each `X86LessonAssertion` and returns `LessonTestResult[]`. 5. POSTs the result to the existing offline-aware completion endpoint (same path Regex uses) so progress and skill awards stay consistent. ### 2.3 X86LessonGraderService New `x86-lesson-grader.service.ts`. Pure function — no DI'd services, no HTTP. Mirrors `RegexLessonGraderService` in shape: - `grade(plan: X86LessonPlan, snapshot: { stdout: string; exitCode: number | null; registers: Record<string, bigint>; }): LessonTestResult[]` - One `LessonTestResult` per leaf assertion (`AllOfAssertion` flattens). PASS/FAIL strings echo the existing `PASS:` / `FAIL:` / `ERROR:` prefix discipline so any downstream parser stays valid. - Includes a small `formatRegisterDiff` helper so failed register assertions render `Expected rax = 0x2A, got rax = 0x00`. ### 2.4 Lesson pane Re-use `RegexLessonPaneComponent` is **not** the right call (regex-specific affordances). Instead, factor a lightweight `<app-x86-lesson-pane>` that renders: - Title, description, instructions list, hints accordion (same shape as Regex pane). - A **dialect picker** (`mat-button-toggle-group` — values constrained to the lesson's `AllowedDialects`). Switching dialect mid-lesson swaps the editor source from the matching starter and emits `select-dialect-for-lesson`. - Submit button (disabled while emulator is `running`). - Test results list reusing the existing test-result chip styles. Tutorials reuse the existing `<app-tutorial-pane>` with no x86-specific code. ### 2.5 Bundle + lazy-load discipline - Lessons UI must lazy-load with the existing route. The Blink WASM module already lazy-loads on first sandbox mount; lesson mode must not eagerly load it on route nav unless the user actually clicks a lesson. - Avoid pulling all 40-50 lesson plan blobs into the bundle. Plans live server-side; the client fetches per-attempt the same way every other playground does. --- ## Phase 3 — Activity tracking + CI - Update the staff-only `/admin/activity-coverage` page to include x86 lesson actions in the registry. - Run `node scripts/check-activity-tracking-drift.cjs` and clear any drift introduced by Phase 2 emissions. - Add an `nx test` spec for `X86LessonRunnerService` (catalog load, lesson open emits the right activity event, dialect switch updates editor + emits event, submit pipeline calls grader and surfaces results). - Add an `nx test` spec for `X86LessonGraderService` covering each assertion variant. --- ## Acceptance criteria - [ ] `LessonGradingRuntime.BlinkX86 = 4` exists and `GetLessonAttemptQueryHandler` includes the runtime in the `BrowserGradingState` projection branch. - [ ] `X86LessonStrategyBase` and `X86LessonPlan` (with `StdoutEqualsAssertion`, `ExitCodeEqualsAssertion`, `RegisterStateAssertion`, `AllOfAssertion`) are defined and unit-tested. - [ ] At least Tiers 0-2 of the curriculum (~12 lessons) ship with reference solutions for every `AllowedDialects` entry, and `X86Curriculum_IntroducedTokensRespectPrerequisites` is green for every reference solution. - [ ] `LessonCategory` includes the ten new x86 categories. - [ ] `LessonCatalogService.listByLanguage` accepts `"x86"`. - [ ] The frontend has a `sandbox` ↔ `lesson` mode toggle in the menubar; sandbox mode is unchanged for existing users. - [ ] Lesson mode renders a chapter sidebar driven by the catalog, opens lessons, switches the assembler dialect, runs Blink, grades results, and reports completion through the same offline-aware path Regex uses. - [ ] All four assertion variants render PASS/FAIL rows in the test results panel, with the same chip styles every other playground uses. - [ ] Lesson mode emits `tools.x86 / lesson-open / lesson-submit / lesson-pass / lesson-fail / lesson-complete / tutorial-complete / select-dialect-for-lesson` events; drift script is green. - [ ] No bundle regression beyond the established x86-playground baseline (record current size in the PR and call out the delta). - [ ] The Tree of Knowledge surfaces x86 lesson actions distinctly from sandbox actions (no aggregation under `compile`/`run`). --- ## References (code) Backend: - `SpikerSoft.Business/Domain/Lessons/ILessonAttemptStrategy.cs` — `LessonGradingRuntime`, `LessonMetadata`, `LessonAttempt`, `LessonAttemptState`. - `SpikerSoft.Business/Domain/Lessons/LessonStrategyBase.cs` — base-class pattern subclasses follow. - `SpikerSoft.Business/Domain/Lessons/LessonCategory.cs` — categories enum to extend. - `SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/RegexLessonStrategyBase.cs` — closest analogue (browser-graded runtime, plan-via-JSON channel). - `SpikerSoft.Business/Domain/Lessons/Curriculum/Regex/RegexLessonPlan.cs` — record-shape pattern for `X86LessonPlan`. - `SpikerSoft.Business/Domain/Lessons/Queries/GetLessonAttempt/GetLessonAttemptQueryHandler.cs` — extend the `RegexBrowser` projection branch to include `BlinkX86`. - `SpikerSoft.Business/Domain/Activity/KnowledgeDomainRegistry.cs` — `tools.x86` definition (line ~195) to extend with lesson actions. - `SpikerSoft.Tests.Unit/Domain/Lessons/Curriculum/RegexReferenceSolutions.cs` — pattern for `X86ReferenceSolutions.cs`. Frontend: - `libraries/features/dev-tools-x86-playground/src/lib/assembly-playground.component.ts` — current shell to add mode-switching to. - `libraries/features/dev-tools-x86-playground/src/lib/services/blink.service.ts` — emulator façade; extend with `snapshotRegisters()` and a "wait for finished/crashed" helper. - `libraries/features/dev-tools-x86-playground/src/lib/services/assembler-config.ts` + `example-snippets.ts` — existing dialect / snippet wiring; lesson starters reuse this `AssemblersKey`. - `libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-runner.service.ts` — closest runner analogue. - `libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-grader.service.ts` — closest grader analogue (browser-only, pure function). - `libraries/features/dev-tools-reg-ex/src/lib/regex-lesson-pane.component.ts` — lesson-pane shape to mirror. - `libraries/features/dev-tools-reg-ex/src/lib/reg-ex.component.ts` — `mode` toggle, sidebar, lesson-load → editor-source pipeline. - `libraries/platform/lesson-catalog/src/lib/lesson-catalog.service.ts` — extend the `language` union to include `"x86"`. - `libraries/shared/lesson-platform/src/lib/lesson-types.ts` — shared types; add an `"x86"` `language` literal to anywhere that constrains it. Pedagogy rules: - `.cursor/rules/curriculum-pedagogy.mdc` - `.cursor/rules/regex-curriculum-pedagogy.mdc` --- ## Open questions / decisions to settle in the PR 1. **CI assembler binaries.** `dotnet test` runs the smoke test for "every reference solution assembles + runs against the assertion". We can either (a) install `as` / `nasm` / `fasm` on the CI image, or (b) drive Blink + the ELF assembler binaries from a Node harness inside `dotnet test` (heavy, but self-contained — Blink is already in the SPA bundle and could be vendored into the test project). Recommend (b) only if (a) bloats CI image size meaningfully; otherwise (a) is the simpler path. 2. **`AllowedDialects` default.** Should an unset list mean "all three" or "must be specified explicitly"? Lean toward explicit — every lesson author thinks about this — but accept either with a code-review nudge. 3. **Register snapshot scope.** Blink already exposes register state to its UI panels. Confirm there's a clean "give me a `Record<string, bigint>` of GP regs" API or add one; do not let the grader poke at internal Blink state. 4. **Linker flag in the test plan.** Some lessons may want to assert exit code without running through `_start` boilerplate. Keep `_start` as the only entry contract for v1; revisit if a lesson would genuinely benefit from a freestanding entry point. 5. **Tutorial dialect coverage.** Tutorials in Tier 0 have to introduce all three dialects without overwhelming the student. Suggest one tutorial per dialect plus one shared "what is x86" tutorial, then default Challenge lessons to "any dialect accepted" until Tier 9 (`X86DialectQuirks`). --- ## Future work (not in this ticket) - Step-debugger lesson kind ("set rax = 42 by stepping through the program; the grader checks the register state at each `int3` breakpoint"). - TypeScript-grader-in-browser for inline disassembly puzzles ("given this hex, what does it disassemble to?"). - ARM64 / RISC-V playground tracks following the same shape. - "Dialect translator" lesson kind: write the program in one dialect, the grader assembles it in all three to verify equivalence.
Author
Owner

implemented

implemented
Sign in to join this conversation.