Using It as a Library

FantASM is a Rust library as well as a command. A program can assemble in-process and get the bytes and the diagnostics back as values, without shelling out and parsing text.

This page is for Rust developers embedding the assembler. Everything else in this documentation is for people writing Z80.

The Dependency

It is not on crates.io. publish = false — FantASM is distributed as signed binaries. Depend on it by path or by git:

[dependencies]
fantasm = { git = "https://codeberg.org/TwistedRaven/fantasm" }

There is no semantic-version promise across those. The crate is versioned for its releases, not for its API.

Default features bring in a lot. nex, sna, lsp and update-check are all on by default, and lsp pulls in tower-lsp and an async runtime. If you only want to assemble:

fantasm = { git = "", default-features = false }

The Assemble Call

use fantasm::assembler::{Assembler, AssemblerOptions};

let mut asm = Assembler::new();
asm.enable_console(false);
asm.device("zxnext");        // implies Z80N; enable_z80n(true) for it on another machine

let outcome = asm.assemble_from_str("    org $8000\nmain:\n    ld a,1\n    ret\n", "demo.asm");

let bytes = asm.emitter.bank.as_slice().to_vec();

assemble(path) does the same from a file, resolving include and incbin against the include path.

Call enable_console(false) first. Without it the assembler prints progress and messages to stdout, which is fatal for anything speaking a protocol there — a language server, or a tool whose output is piped.

The machine is device since 2.0, on this surface as well as the command line: Assembler::device() in place of Assembler::target(), and Options::device in place of Options::target. target still means an image the project builds — target_name, Options::targets — so the two are no longer one word.

verbosity(level) sets how much is printed0 nothing, 1 the progress log, 2 what the build decided. enable_console(bool) is unchanged and still means one level, which is what it always meant.

Diagnostics

This is the part that catches people. The Result carries one error; the collected set lives on the assembler:

let _ = asm.assemble_from_str(source, "demo.asm");   // both consumers ignore this
for e in &asm.errors   { /* … */ }
for w in &asm.warnings { /* … */ }
asm.errorsEvery error the build found. Two undefined names give two entries.
asm.warningsWarnings, and they accumulate.

The Result’s single error is the first one; it is there so ? works, not because it is all there was.

warnings is not only warnings. !message output (Message, 1056) is in the same list, and so is the shebang notice (Info, 1057). Filter on level, never on the code:

use fantasm::assembler::ErrorLevel;

// `display_unused` takes &mut, so ask for the unused-label sweep before borrowing the list.
asm.display_unused();

let real: Vec<_> = asm.warnings.iter()
    .filter(|w| matches!(w.level, ErrorLevel::Warning | ErrorLevel::Error))
    .collect();

W1049 unused label needs asking for. display_unused() is the library equivalent of --find-unused, and it appends to warnings — so call it before you collect, or the borrow checker will say so for you.

Each Diagnostic carries level, error_code, message, hint, location (file, line, column) and related_locations. Nothing needs parsing out of a string — see Diagnostics for what the codes mean.

Test Results

execute_tests() answers with one error counting the failures, so the same rule applies — the detail is on the assembler:

let _ = asm.execute_tests();
asm.test_counts.passed;              // and .failed
for f in &asm.test_failures { /* an ordinary Diagnostic */ }

A failing assertion is E1091, positioned at the assertion’s own line. Anything else that went wrong while the body assembled keeps the code and hint it would have had in the program — see Testing.

Debug Information

Source-to-address records are on asm.sld_records as values, so you need not read the file back.

They are only populated if you nominate a file. sld_file() is what turns recording on; without it sld_records is empty, and with it a .sld is written whether you want one or not:

let scratch = std::env::temp_dir().join("mine.sld");
asm.sld_file(scratch.to_string_lossy().as_ref());
// … assemble …
let traces: Vec<_> = asm.sld_records.iter().filter(|r| r.record_type == "T").collect();
let _ = std::fs::remove_file(&scratch);

T records map a source line to an address; L records are symbols. See Debugging a Build for the format.

The Tokeniser

fantasm::assembler::tokens::{Token, TokenReader} is the lexer the assembler uses, and can be run without assembling anything — which is how an editor gets highlighting that agrees with the assembler rather than approximating it with regular expressions.

Who Uses This

0x1DE, the native IDE, embeds it twice: assemble runs a build in-process and turns the diagnostics into its own type, and lexer uses the tokeniser alone for highlighting. Between them that is Assembler, AssemblerOptions, Diagnostic, ErrorLevel and TokenReader — a small surface, and the one most likely to keep working, being the one something exercises.

Reaching further in is possible, since much of the crate is pub, but nothing outside promises to stay put.

Further Reading

Documentation