Skip to content

Commit

Permalink
Use a full expression parser.
Browse files Browse the repository at this point in the history
Now ORIGIN and LENGTH can use `(64K + 4K) - 8K` and it will just work.

Fixes #103
Fixes #101
Fixes #66
  • Loading branch information
jonathanpallant committed Jan 7, 2025
1 parent f184110 commit 5178bca
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

- [#102] Add support for -h/--help arg and no arg
- [xxx] Add a full expression parser for ORIGIN and LENGTH

[#102]: https://github.com/knurling-rs/flip-link/pull/102
[xxx]: https://github.com/knurling-rs/flip-link/pull/xxx

## [v0.1.9] - 2024-08-14

Expand Down
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ readme = "README.md"

[dependencies]
env_logger = { version = "0.11", default-features = false }
evalexpr = "12.0.2"
getrandom = "0.2"
log = "0.4"
object = { version = "0.35", default-features = false, features = ["read_core", "elf", "std"] }
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,37 @@ NOTE that if you were using GNU `ld` or GNU `gcc` to link your program then this
## Testing

Our CI enforces various checks. You can run them locally to make sure your PR will pass the CI:

* `cargo fmt --all -- --check`
* `cargo clippy -- --deny warnings`
* `cargo xtest`
* This installs the current revision of `flip-link` and runs `cargo test`.

## Logging

If you want to see what `flip-link` is up to, you can set these environment variables:

```bash
export RUSTC_LOG=rustc_codegen_ssa::back::link=info
export RUST_LOG=info
```

This will produce something like:

```console
$ cargo build
...
INFO rustc_codegen_ssa::back::link linker stderr:
[INFO flip_link] found MemoryEntry(line=3, origin=0x20000000, length=0x10000) in ./target/thumbv7em-none-eabi/debug/build/lm3s6965-3b7087c63b161e04/out/memory.x
[INFO flip_link] used RAM spans: origin=0x20000000, length=12, align=4
[INFO flip_link] new RAM region: ORIGIN=0x2000fff0, LENGTH=16
INFO rustc_codegen_ssa::back::link linker stdout:
INFO rustc_codegen_ssa::back::link linker stdout:
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
```

You can see even more detail about how we parse expressions using `RUST_LOG=debug`.

## Support

`flip-link` is part of the [Knurling] project, [Ferrous Systems]' effort at
Expand Down
150 changes: 107 additions & 43 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fn notmain() -> Result<i32> {
for linker_script in linker_scripts {
let script_contents = fs::read_to_string(linker_script.path())?;
if let Some(entry) = find_ram_in_linker_script(&script_contents) {
log::debug!("found {entry:?} in {}", linker_script.path().display());
log::info!("found {entry} in {}", linker_script.path().display());
ram_path_entry = Some((linker_script, entry));
break;
}
Expand Down Expand Up @@ -257,6 +257,16 @@ impl MemoryEntry {
}
}

impl std::fmt::Display for MemoryEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"MemoryEntry(line={}, origin=0x{:08x}, length=0x{:x})",
self.line, self.origin, self.length
)
}
}

/// Rm `token` from beginning of `line`, else `continue` loop iteration
macro_rules! eat {
($line:expr, $token:expr) => {
Expand Down Expand Up @@ -303,14 +313,14 @@ fn find_ram_in_linker_script(linker_script: &str) -> Option<MemoryEntry> {
line = eat!(line, '=');

let boundary_pos = tryc!(line.find(',').ok_or(()));
let origin = perform_addition(&line[..boundary_pos]);
let origin = evalulate_expression(&line[..boundary_pos]) as u64;
line = line[boundary_pos..].trim();

line = eat!(line, ',');
line = eat!(line, "LENGTH");
line = eat!(line, '=');

let length = perform_addition(line);
let length = evalulate_expression(line) as u64;

return Some(MemoryEntry {
line: index,
Expand All @@ -322,41 +332,76 @@ fn find_ram_in_linker_script(linker_script: &str) -> Option<MemoryEntry> {
None
}

/// Perform addition when ORIGN or LENGTH variables contain an addition.
/// If there is no addition to be performed, it will return the `u64` value.
fn perform_addition(line: &str) -> u64 {
let segments = line.split('+').map(str::trim).collect::<Vec<_>>();

let mut total_length = 0;
for segment in segments {
// Split number and the optional unit
let (number, unit) = match segment.find(['K', 'M']) {
Some(unit_pos) => {
let (number, unit) = segment.split_at(unit_pos);
(number, unit.chars().next())
}
None => (segment, None),
};

// Parse number
let (number, radix) = match number.strip_prefix("0x") {
Some(s) => (s, 16),
None => (number, 10),
};
let length = tryc!(u64::from_str_radix(number, radix));

// Handle unit
let multiplier = match unit {
Some('K') => 1024,
Some('M') => 1024 * 1024,
None => 1,
_ => unreachable!(),
};

// Add length
total_length += length * multiplier;
/// Parse an integer, with an optional multiplication suffix
fn parse_integer(input: &str) -> Option<u64> {
log::debug!("Parsing {input:?}");
let input = input.trim();
let mut multiplier = 1;
let numeric = if let Some(n) = input.strip_suffix('K') {
log::debug!("Is Kibibyte");
multiplier = 1024;
n
} else if let Some(n) = input.strip_suffix('M') {
log::debug!("Is Mebibyte");
multiplier = 1024 * 1024;
n
} else if let Some(n) = input.strip_suffix('G') {
log::debug!("Is Gibibyte");
multiplier = 1024 * 1024 * 1024;
n
} else {
log::debug!("No suffix");
input
};
log::debug!("{numeric} x {multiplier}");

// Parse number
let (number, radix) = match numeric.strip_prefix("0x") {
Some(s) => (s, 16),
None => (numeric, 10),
};
let value = u64::from_str_radix(number, radix).ok()?;

Some(value * multiplier)
}

/// Evaluate a linker-script expression.
///
/// Panics if the expression is not understood
fn evalulate_expression(line: &str) -> i64 {
log::debug!("Evaluating expression {:?}", line);

// We cannot handle '(x)' but we can handle ' ( x + 1 ) '.
// These extra spaces have no affect on the arithmetic; they just
// make it easier for us to find the suffixes
let line = line.replace("(", " ( ").replace(")", " ) ");

let mut processed_segments = Vec::new();
// deal with all the suffixes that evalexpr doesn't know
for segment in line.split_whitespace() {
// is it numeric? If so, process any suffix it has before the evaluator sees it
if let Some(number) = parse_integer(segment) {
processed_segments.push(format!("{number}"))
} else {
processed_segments.push(segment.to_string());
}
}
total_length

log::debug!("Split expression into {processed_segments:?}");
let processed_string = processed_segments.join(" ");
let value = match evalexpr::eval(&processed_string) {
Ok(evalexpr::Value::Int(n)) => n,
Ok(val) => {
panic!("Failed to parse expression {line:?} ({processed_string:?}), got {val:?}?");
}
Err(e) => {
panic!("Failed to parse expression {line:?} ({processed_string:?}), got error {e:?}");
}
};

log::debug!("Evaluated expression as {:?}", value);

value
}

#[cfg(test)]
Expand All @@ -365,6 +410,7 @@ mod tests {

#[test]
fn parse() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
Expand All @@ -390,6 +436,7 @@ mod tests {

#[test]
fn parse_no_units() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 262144
Expand All @@ -415,6 +462,7 @@ mod tests {

#[test]
fn ingore_comment() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
Expand All @@ -440,26 +488,38 @@ mod tests {

#[test]
fn test_perform_addition_hex_and_number() {
_ = env_logger::try_init();
const ADDITION: &str = "0x20000000 + 1000";
let expected: u64 = 0x20000000 + 1000;
let expected: i64 = 0x20000000 + 1000;

assert_eq!(evalulate_expression(ADDITION), expected);
}

#[test]
fn test_perform_complex_maths() {
_ = env_logger::try_init();
const ADDITION: &str = "(0x20000000 + 1K) - 512";
let expected: i64 = 0x20000000 + 512;

assert_eq!(perform_addition(ADDITION), expected);
assert_eq!(evalulate_expression(ADDITION), expected);
}

#[test]
fn test_perform_addition_returns_number() {
_ = env_logger::try_init();
const NO_ADDITION: &str = "0x20000000";
let expected: u64 = 536870912; //0x20000000 base 10
let expected: i64 = 0x20000000;

assert_eq!(perform_addition(NO_ADDITION), expected);
assert_eq!(evalulate_expression(NO_ADDITION), expected);
}

#[test]
fn parse_plus() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2M
RAM : ORIGIN = 0x20020000, LENGTH = 368K + 16K
RAM : ORIGIN = 0x20000000 + 0x20000, LENGTH = 512K - 256K
}
INCLUDE device.x";
Expand All @@ -469,7 +529,7 @@ mod tests {
Some(MemoryEntry {
line: 3,
origin: 0x20020000,
length: (368 + 16) * 1024,
length: (512 - 256) * 1024,
})
);

Expand All @@ -481,6 +541,7 @@ mod tests {

#[test]
fn parse_plus_origin_k() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2M
Expand All @@ -506,6 +567,7 @@ mod tests {

#[test]
fn parse_plus_origin_no_units() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2M
Expand All @@ -531,6 +593,7 @@ mod tests {

#[test]
fn parse_plus_origin_m() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 2M
Expand All @@ -557,6 +620,7 @@ mod tests {
// test attributes https://sourceware.org/binutils/docs/ld/MEMORY.html
#[test]
fn parse_attributes() {
_ = env_logger::try_init();
const LINKER_SCRIPT: &str = "MEMORY
{
/* NOTE 1 K = 1 KiBi = 1024 bytes */
Expand Down

0 comments on commit 5178bca

Please sign in to comment.