summaryrefslogtreecommitdiff
path: root/src/lib.rs
blob: d45d449a190d71652c7f8b7414503b8790dcc372 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#![feature(path_add_extension)]

mod formats;
mod types;
mod stages;
pub use formats::*;
pub use types::*;
pub use stages::*;

use assembler::*;
use log::*;
use switchboard::*;

use std::io::Read;
use std::io::Write;


pub const RETURN_MODE:    u8 = 0x80;
pub const WIDE_MODE:      u8 = 0x40;
pub const IMMEDIATE_MODE: u8 = 0x20;


pub fn assemble(mut args: Switchboard, invocation: &str) {
    args.positional("source");
    args.positional("destination");
    args.named("extension").default("brc");

    args.named("no-libs");
    args.named("no-project-libs");
    args.named("no-env-libs");
    args.named("no-truncate");

    args.named("format").default("raw");
    args.named("dry-run").short('n');
    args.named("tree");
    args.named("with-symbols");
    args.named("help").short('h');
    args.raise_errors();

    if args.get("help").as_bool() {
        print_help(invocation);
        std::process::exit(0);
    }

    let source_path        = args.get("source").as_path_opt().map(
        |p| p.canonicalize().unwrap_or_else(|e| fatal!("{p:?}: {e:?}")));
    let destination_path   = args.get("destination").as_path_opt();
    let extension          = args.get("extension").as_string();
    let opt_extension      = Some(extension.as_str());

    let no_libs            = args.get("no-libs").as_bool();
    let no_project_libs    = args.get("no-project-libs").as_bool();
    let no_env_libs        = args.get("no-env-libs").as_bool();
    let no_truncate        = args.get("no-truncate").as_bool();

    let format             = Format::from_str(args.get("format").as_str());
    let dry_run            = args.get("dry-run").as_bool();
    let print_tree         = args.get("tree").as_bool();
    let export_symbols     = args.get("with-symbols").as_bool();

    // -----------------------------------------------------------------------

    let mut compiler = new_compiler();

    if let Some(path) = &source_path {
        info!("Reading program source from {path:?}");
        compiler.root_from_path(path).unwrap_or_else(|err| fatal!("{err:?}: {path:?}"));
    } else {
        let mut source_code = String::new();
        info!("Reading program source from standard input");
        if let Err(err) = std::io::stdin().read_to_string(&mut source_code) {
            fatal!("Could not read from standard input\n{err:?}");
        }
        compiler.root_from_string(source_code, "<standard input>")
    };
    if compiler.error().is_some() && !no_libs && !no_project_libs {
        compiler.include_libs_from_parent(opt_extension);
    }
    if compiler.error().is_some() && !no_libs && !no_env_libs {
        compiler.include_libs_from_path_variable("BEDROCK_LIBS", opt_extension);
    }

    if print_tree {
        compiler.hierarchy().report()
    }
    if let Some(error) = compiler.error() {
        error.report();
        std::process::exit(1);
    }

    let merged_source = compiler.get_compiled_source().unwrap_or_else(|error| {
        error.report();
        std::process::exit(1);
    });

    if !dry_run && format == Format::Source {
        write_bytes_and_exit(merged_source.as_bytes(), destination_path.as_ref());
    }

    // -----------------------------------------------------------------------

    let path = Some("<merged source>");
    let syntactic = match parse_syntactic(&merged_source, path) {
        Ok(tokens) => tokens,
        Err(errors) => {
            report_syntactic_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let semantic = match parse_semantic(syntactic) {
        Ok(tokens) => tokens,
        Err(errors) => {
            report_semantic_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let program = match generate_bytecode(&semantic) {
        Ok(program) => program,
        Err(errors) => {
            report_bytecode_errors(&errors, &merged_source);
            std::process::exit(1);
        }
    };

    let AssembledProgram { mut bytecode, symbols } = program;

    let length = bytecode.len();
    let percentage = (length as f32 / 65536.0 * 100.0).round() as u16;
    info!("Assembled program in {length} bytes ({percentage}% of maximum)");

    if !no_truncate {
        // Remove null bytes from end of bytecode.
        while let Some(0) = bytecode.last() {
            bytecode.pop();
        }
        let difference = length - bytecode.len();
        if difference > 0 {
            info!("Truncated program to {length} bytes (saved {difference} bytes)");
        }
    }

    if !dry_run {
        if export_symbols {
            if let Some(path) = &destination_path {
                let mut symbols_path = path.to_path_buf();
                symbols_path.add_extension("sym");
                let mut symbols_string = String::new();
                for symbol in &symbols {
                    let address = &symbol.address;
                    let name = &symbol.name;
                    let location = &symbol.source.location();
                    symbols_string.push_str(&format!(
                        "{address:04x} {name} {location}\n"
                    ));
                }
                match std::fs::write(&symbols_path, symbols_string) {
                    Ok(_) => info!("Saved symbols to {symbols_path:?}"),
                    Err(err) => info!("Could not write symbols to {symbols_path:?}\n{err:?}"),
                }
            }
        }

        let bytes = match format {
            Format::Raw => bytecode,
            Format::Clang => format_clang(&bytecode),
            Format::Source => unreachable!("Source output is handled before full assembly"),
        };
        write_bytes_and_exit(&bytes, destination_path.as_ref());
    }
}


fn write_bytes_and_exit<P: AsRef<Path>>(bytes: &[u8], path: Option<&P>) -> ! {
    match path {
        Some(path) => match std::fs::write(path, bytes) {
            Ok(_) => info!("Wrote output to {:?}", path.as_ref()),
            Err(err) => fatal!("Could not write to {:?}\n{err:?}", path.as_ref()),
        }
        None => match std::io::stdout().write_all(bytes) {
            Ok(_) => info!("Wrote output to standard output"),
            Err(err) => fatal!("Could not write to standard output\n{err:?}"),
        }
    }
    std::process::exit(0);
}


fn print_help(invocation: &str) {
    eprintln!("\
Usage: {invocation} [source] [destination]

Convert Bedrock source code into an assembled Bedrock program.

Usage:
  To assemble a Bedrock program from a source file and write to an output
  file, run `br-asm [source] [destination]`, where [source] is the path
  of the source file and [destination] is the path to write to.

  If [destination] is omitted, the assembled program will be written to
  standard output. If [source] is omitted, the program source code will
  be read from standard input.

Environment variables:
  BEDROCK_LIBS
    A list of colon-separated paths which will be searched to find Bedrock
    source code files to use as libraries when assembling a Bedrock program.
    If a library file resolves an unresolved symbol in the program being
    assembled, the library file will be merged into the program.

Arguments:
  [source]               Bedrock source code file to assemble.
  [destination]          Destination path for assembler output.

Switches:
  --dry-run        (-n)  Assemble and show errors only, don't write any output
  --extension=<ext>      File extension to identify source files (default is 'brc')
  --format=<fmt>         Output format to use for assembled program (default is 'raw')
  --no-project-libs      Don't search for libraries in the source parent folder
  --no-env-libs          Don't search for libraries in the BEDROCK_LIBS path variable
  --no-libs              Combination of --no-project-libs and --no-env-libs
  --no-truncate          Don't remove trailing zero-bytes from the assembled program
  --tree                 Show a tree diagram of all included library files
  --with-symbols         Also generate debug symbols file with extension '.sym'
  --help           (-h)  Print this help information
  --verbose,       (-v)  Print additional information
  --version              Print the program version and exit
");
}