Skip to content

Rewrite backend: single-pass fasm pipeline (drops qbe/as/ld.lld) - #69

Closed
Prestgg1 wants to merge 1 commit into
mainfrom
fasm-backend
Closed

Prestgg1 wants to merge 1 commit into
mainfrom
fasm-backend

Conversation

@Prestgg1

@Prestgg1 Prestgg1 commented Oct 7, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the whole code-generation/linking chain (qbe → .il → as → .o → ld.lld) with a single-pass fasm pipeline: the compiler emits one .asm file, and fasm assembles and links it directly into a standalone ELF. No object files, no separate linker, no LLVM/QBE toolchain — the only external dependency is fasm.

Before After
qbe + as + ld.lld fasm (one invocation)
.il → .s → .o → binary .asm → binary
@link = prebuilt .o files + sdk/build.sh @link = fasm source include (no build step)

What's changed

Backend

  • crates/transpiler: full fasm emitter — stack-slot frame model (16-byte aligned), System V AMD64 call ABI (≤6 args in registers, overflow on stack), azfn_ user-function labels (avoids fasm reserved-word collisions like add), string data dedupe, fasm local branch labels, escape sequences (\\n, \\t, \\r, \\0, \\\\) resolved at emission
  • validator: link_files is now (operation, source path); only operations actually called get their source included, each path exactly once
  • compiler: builds a single .asm (header → filtered includes → code → data) and runs fasm; BackendError::Fasm surfaces fasm stderr
  • sdk: assembly stubs are fasm syntax, lib.az @link → ./sdk/src/*.s, build.sh removed

Bugs fixed along the way (found by e2e)

  • tokenizer: &&/|| caused an infinite loop in read_operator; =/! peek guard ate the first char of the next token (!flag → lag)
  • parser: f() + g() / return f() + g() fell through the call path; loop bodies were never parsed
  • validator: loop bodies were never validated
  • string escapes: \\n etc. never worked in data emission (old QBE path handled it)

Tests
The suite did not compile at main HEAD (38 errors, pre-existing — verified against a clean worktree). Repaired the validator/tokenizer tests against the current API; all 18 test suites are green now.

Tooling & docs

  • CI: installs fasm, checks out submodules
  • .gitignore: *.ssa, *.asm, /sdk/ anchoring (azlang-docs/src/sdk/*.md was accidentally ignored), book output; tracked compiler binaries/.s/.ssa artifacts removed from git
  • docs: qbe.md → fasm.md, assembly_linking.md → direct_assembly.md, all @link examples → ./sdk/src/*.s, diagrams/pipeline rewritten, mdbook build passes

Measured (not estimates)

Program Size
examples/hello_world.az 198 bytes
Docs hello world (own @link, escapes) 228 bytes
60-line multi-module integration program 2113 bytes

file: ELF 64-bit LSB executable, x86-64, statically linked, no section header — 1 program header, no dynamic interpreter.

⚠️ Merge order

Companion PR for the SDK submodule must merge first: AzLangProgramingLanguage/AzLang-Std#1 (this PR's sdk gitlink points at 273f2b0 on its fasm-sdk branch).

Testing

  • cargo test --workspace — 18 suites, all green
  • azcli build + run for: hello world (198B), docs example (228B), integration program covering modules, recursion, while/loop/break/continue, if/elif/else, div/mod, &&, string ==, exit — all assertions pass, exit 0
  • mdbook build passes

Compiler:
- transpiler: emit fasm x86_64 assembly directly (stack-slot frame model,
  System V call ABI, azfn_ label prefix, string data dedupe, fasm local
  branch labels, \\n/\\t/\\r/\\0/\\\\ escapes resolved at emission)
- validator: link_files is now (operation, source path); only invoked
  @link operations are included, each path exactly once
- pipline: single fasm invocation, BackendError::Fasm; qbe/as/ld.lld,
  .il/.ssa/.o intermediate stages and the external linker are gone
- compiler: builds one .asm (header, filtered includes, code, data) and
  fasm assembles+links it straight into a standalone ELF

Parser/tokenizer fixes found by e2e:
- &&/|| dispatch (previously an infinite loop in read_operator)
- '='/'!' peek guards (!flag parsed the identifier away)
- binary op over call results (f() + g()) fell through the LParen path
- loop bodies parsed as statements (previously never parsed their body)
- validator now validates loop bodies

Tests: repair the broken-at-HEAD suite (38 compile errors), stale func
keyword test, duplicate-func expectation -> all 18 test suites green

Tooling/docs:
- CI: install fasm, checkout submodules
- gitignore: *.ssa, *.asm, /sdk/ fix (azlang-docs/src/sdk/ was ignored),
  book output; git rm tracked compiler binaries and .s/.ssa artifacts
- docs: qbe.md -> fasm.md, assembly_linking.md -> direct_assembly.md,
  real measured sizes (198/228 byte hello world), ./sdk/src/*.s @link
@Prestgg1

Prestgg1 commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

it looks great but I can't merge this pr. Because it's vibe coded

@Prestgg1 Prestgg1 closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant