From 0fc55bbd80c75196c8d702c2267a2c59e6a3e2d3 Mon Sep 17 00:00:00 2001 From: Angie Date: Wed, 14 Aug 2024 19:23:53 -0400 Subject: [PATCH] Support for custom asserts --- CHANGELOG.md | 3 + README.md | 5 ++ docs/file_format/README.md | 1 + docs/file_format/asserts.md | 64 +++++++++++++++++++ slinky/src/absent_nullable.rs | 2 +- slinky/src/assert_entry.rs | 75 +++++++++++++++++++++++ slinky/src/document.rs | 17 +++-- slinky/src/lib.rs | 2 + slinky/src/linker_writer.rs | 48 ++++++++++++++- slinky/src/partial_linker_writer.rs | 8 ++- slinky/src/script_buffer.rs | 2 +- slinky/src/traits.rs | 13 ++-- tests/partial_linking/follow_segment.ld | 2 + tests/partial_linking/follow_segment.yaml | 4 ++ tests/partial_linking/required_syms.ld | 4 +- tests/test_cases/archives.ld | 2 + tests/test_cases/archives.yaml | 7 +++ tests/test_cases/drmario64.ld | 2 + tests/test_cases/drmario64.yaml | 4 ++ tests/test_cases/required_syms.ld | 4 +- 20 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 docs/file_format/asserts.md create mode 100644 slinky/src/assert_entry.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a9d998..2774215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Can't be combined with the global `hardcoded_gp_value`. - Add new top-level attribute for the file format: `entry`. - Specifies the entrypoint for the build. +- Add new top-level attribute for the file format: `asserts`. + - Allows to define multiple assertions that should be satisfied for the link + to success. ### Changed diff --git a/README.md b/README.md index 6390776..a8f6db5 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,11 @@ extra shiftability features not present on other tools. - Support for both listing a library and letting the linker grab any used file or to specify which speicific files should be linked by the linker. - Support splat's "vram classes" for better memory layout management. +- Support emitting custom symbols and requiring some symbols to be defined on + during linking. +- Support for defining a shiftable `_gp` for small data support. +- Support for defining the entrypoint of the executable. +- Support for defining asserts to ensure the sanity of the build. ### Planned features diff --git a/docs/file_format/README.md b/docs/file_format/README.md index e8a1188..ae7156a 100644 --- a/docs/file_format/README.md +++ b/docs/file_format/README.md @@ -18,6 +18,7 @@ The document is composed by the following top-level attributes: - A single optional string that specifies the entrypoint of the final build. - A list of [`symbol_assignments`](symbol_assignments.md). - A list of [`required_symbols`](required_symbols.md). +- A list of [`asserts`](asserts.md) ## Example diff --git a/docs/file_format/asserts.md b/docs/file_format/asserts.md new file mode 100644 index 0000000..3e7dfc5 --- /dev/null +++ b/docs/file_format/asserts.md @@ -0,0 +1,64 @@ +# Asserts + +An assert ensures that a given condition is satified by the linking process, +otherwise fail the link. + +GNU LD documentation for +[`ASSERT`](https://sourceware.org/binutils/docs/ld/Miscellaneous-Commands.html#index-ASSERT) + +Every attribute listed is optional unless explicitly stated. + +## Table of contents + +- [Asserts](#asserts) + - [Table of contents](#table-of-contents) + - [`check`](#check) + - [Example](#example) + - [Valid values](#valid-values) + - [error\_message](#error_message) + - [Example](#example-1) + - [Valid values](#valid-values-1) + - [`include_if_any`, `include_if_all`, `exclude_if_any` and `exclude_if_all`](#include_if_any-include_if_all-exclude_if_any-and-exclude_if_all) + +## `check` + +This field is **required**. + +The actual condition to check. If this check evaluates to zero then the linker +exits with an error code and prints [`error_message`](#error_message). + +### Example + +```yaml +asserts: + - check: boot_ROM_END <= 0x101000 + error_message: boot segment is larger than 1 MiB +``` + +### Valid values + +Non empty string. + +## error_message + +The error message to show if [`check`](#check) is not satisfied. + +### Example + +```yaml +asserts: + - check: boot_VRAM_END <= 0x80400000 + error_message: VRAM is larger than 4 MiB + include_if_any: [[ram_size, 4]] +``` + +### Valid values + +Non empty string. + +## `include_if_any`, `include_if_all`, `exclude_if_any` and `exclude_if_all` + +These fields allow to conditionally include or exclude a given segment depending +on the current [custom options](custom_options.md). + +Their syntax is the same as their [`file`](file.md#include_if_any) counterparts. diff --git a/slinky/src/absent_nullable.rs b/slinky/src/absent_nullable.rs index b351bff..ad6a671 100644 --- a/slinky/src/absent_nullable.rs +++ b/slinky/src/absent_nullable.rs @@ -7,7 +7,7 @@ use crate::SlinkyError; // https://stackoverflow.com/a/44332837/6292472 -#[derive(Debug, PartialEq, Default)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] pub(crate) enum AbsentNullable { #[default] Absent, diff --git a/slinky/src/assert_entry.rs b/slinky/src/assert_entry.rs new file mode 100644 index 0000000..442ae6c --- /dev/null +++ b/slinky/src/assert_entry.rs @@ -0,0 +1,75 @@ +/* SPDX-FileCopyrightText: © 2024 decompals */ +/* SPDX-License-Identifier: MIT */ + +use serde::Deserialize; + +use crate::{absent_nullable::AbsentNullable, traits::Serial, Settings, SlinkyError}; + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct AssertEntry { + pub check: String, + pub error_message: String, + + pub include_if_any: Vec<(String, String)>, + pub include_if_all: Vec<(String, String)>, + pub exclude_if_any: Vec<(String, String)>, + pub exclude_if_all: Vec<(String, String)>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct AssertEntrySerial { + pub check: String, + pub error_message: String, + + #[serde(default)] + pub include_if_any: AbsentNullable>, + #[serde(default)] + pub include_if_all: AbsentNullable>, + #[serde(default)] + pub exclude_if_any: AbsentNullable>, + #[serde(default)] + pub exclude_if_all: AbsentNullable>, +} + +impl Serial for AssertEntrySerial { + type Output = AssertEntry; + + fn unserialize(self, _settings: &Settings) -> Result { + if self.check.is_empty() { + return Err(SlinkyError::EmptyValue { + name: "check".to_string(), + }); + } + let check = self.check; + + if self.error_message.is_empty() { + return Err(SlinkyError::EmptyValue { + name: "error_message".to_string(), + }); + } + let error_message = self.error_message; + + let include_if_any = self + .include_if_any + .get_non_null_not_empty("include_if_any", Vec::new)?; + let include_if_all = self + .include_if_all + .get_non_null_not_empty("include_if_all", Vec::new)?; + let exclude_if_any = self + .exclude_if_any + .get_non_null_not_empty("exclude_if_any", Vec::new)?; + let exclude_if_all = self + .exclude_if_all + .get_non_null_not_empty("exclude_if_all", Vec::new)?; + + Ok(Self::Output { + check, + error_message, + include_if_any, + include_if_all, + exclude_if_any, + exclude_if_all, + }) + } +} diff --git a/slinky/src/document.rs b/slinky/src/document.rs index 3784551..0093383 100644 --- a/slinky/src/document.rs +++ b/slinky/src/document.rs @@ -6,10 +6,10 @@ use std::{fs, path::Path}; use serde::Deserialize; use crate::{ - absent_nullable::AbsentNullable, required_symbol::RequiredSymbolSerial, segment::SegmentSerial, - settings::SettingsSerial, symbol_assignment::SymbolAssignmentSerial, traits::Serial, - vram_class::VramClassSerial, RequiredSymbol, Segment, Settings, SlinkyError, SymbolAssignment, - VramClass, + absent_nullable::AbsentNullable, assert_entry::AssertEntrySerial, + required_symbol::RequiredSymbolSerial, segment::SegmentSerial, settings::SettingsSerial, + symbol_assignment::SymbolAssignmentSerial, traits::Serial, vram_class::VramClassSerial, + AssertEntry, RequiredSymbol, Segment, Settings, SlinkyError, SymbolAssignment, VramClass, }; #[derive(PartialEq, Debug)] @@ -23,6 +23,7 @@ pub struct Document { pub entry: Option, pub symbol_assignments: Vec, pub required_symbols: Vec, + pub asserts: Vec, } impl Document { @@ -66,6 +67,8 @@ pub(crate) struct DocumentSerial { pub symbol_assignments: AbsentNullable>, #[serde(default)] pub required_symbols: AbsentNullable>, + #[serde(default)] + pub asserts: AbsentNullable>, } impl DocumentSerial { @@ -100,6 +103,11 @@ impl DocumentSerial { .get_non_null("required_symbols", Vec::new)? .unserialize(&settings)?; + let asserts = self + .asserts + .get_non_null("asserts", Vec::new)? + .unserialize(&settings)?; + Ok(Document { settings, vram_classes, @@ -107,6 +115,7 @@ impl DocumentSerial { entry, symbol_assignments, required_symbols, + asserts, }) } } diff --git a/slinky/src/lib.rs b/slinky/src/lib.rs index 3e8c39a..18d86a7 100644 --- a/slinky/src/lib.rs +++ b/slinky/src/lib.rs @@ -10,6 +10,7 @@ mod utils; mod linker_symbols_style; mod settings; +mod assert_entry; mod file_info; mod file_kind; mod gp_info; @@ -36,6 +37,7 @@ pub use escaped_path::EscapedPath; pub use linker_symbols_style::LinkerSymbolsStyle; pub use settings::Settings; +pub use assert_entry::AssertEntry; pub use file_info::FileInfo; pub use file_kind::FileKind; pub use required_symbol::RequiredSymbol; diff --git a/slinky/src/linker_writer.rs b/slinky/src/linker_writer.rs index 6e26658..f9fc176 100644 --- a/slinky/src/linker_writer.rs +++ b/slinky/src/linker_writer.rs @@ -4,9 +4,9 @@ use std::io::Write; use crate::{ - utils, version, Document, EscapedPath, FileInfo, FileKind, RequiredSymbol, RuntimeSettings, - ScriptExporter, ScriptGenerator, ScriptImporter, Segment, SlinkyError, SymbolAssignment, - VramClass, + utils, version, AssertEntry, Document, EscapedPath, FileInfo, FileKind, RequiredSymbol, + RuntimeSettings, ScriptExporter, ScriptGenerator, ScriptImporter, Segment, SlinkyError, + SymbolAssignment, VramClass, }; use crate::script_buffer::ScriptBuffer; @@ -137,6 +137,20 @@ impl ScriptImporter for LinkerWriter<'_> { Ok(()) } + + fn add_all_asserts(&mut self, asserts: &[AssertEntry]) -> Result<(), SlinkyError> { + if asserts.is_empty() { + return Ok(()); + } + + self.begin_asserts()?; + for assert_entry in asserts { + self.add_assert(assert_entry)?; + } + self.end_asserts()?; + + Ok(()) + } } impl ScriptExporter for LinkerWriter<'_> { @@ -682,6 +696,34 @@ impl LinkerWriter<'_> { Ok(()) } + + pub(crate) fn begin_asserts(&mut self) -> Result<(), SlinkyError> { + if !self.buffer.is_empty() { + self.buffer.write_empty_line(); + } + + Ok(()) + } + + pub(crate) fn end_asserts(&mut self) -> Result<(), SlinkyError> { + Ok(()) + } + + pub(crate) fn add_assert(&mut self, assert_entry: &AssertEntry) -> Result<(), SlinkyError> { + if !self.rs.should_emit_entry( + &assert_entry.exclude_if_any, + &assert_entry.exclude_if_all, + &assert_entry.include_if_any, + &assert_entry.include_if_all, + ) { + return Ok(()); + } + + self.buffer + .write_assert(&assert_entry.check, &assert_entry.error_message); + + Ok(()) + } } // internal functions diff --git a/slinky/src/partial_linker_writer.rs b/slinky/src/partial_linker_writer.rs index d111515..6a42536 100644 --- a/slinky/src/partial_linker_writer.rs +++ b/slinky/src/partial_linker_writer.rs @@ -2,8 +2,8 @@ /* SPDX-License-Identifier: MIT */ use crate::{ - Document, EscapedPath, FileInfo, LinkerWriter, RequiredSymbol, RuntimeSettings, ScriptExporter, - ScriptGenerator, ScriptImporter, Segment, SlinkyError, SymbolAssignment, + AssertEntry, Document, EscapedPath, FileInfo, LinkerWriter, RequiredSymbol, RuntimeSettings, + ScriptExporter, ScriptGenerator, ScriptImporter, Segment, SlinkyError, SymbolAssignment, }; pub struct PartialLinkerWriter<'a> { @@ -93,6 +93,10 @@ impl ScriptImporter for PartialLinkerWriter<'_> { ) -> Result<(), SlinkyError> { self.main_writer.add_all_required_symbols(required_symbols) } + + fn add_all_asserts(&mut self, asserts: &[AssertEntry]) -> Result<(), SlinkyError> { + self.main_writer.add_all_asserts(asserts) + } } impl ScriptExporter for PartialLinkerWriter<'_> { diff --git a/slinky/src/script_buffer.rs b/slinky/src/script_buffer.rs index 3cf37cc..9fdc452 100644 --- a/slinky/src/script_buffer.rs +++ b/slinky/src/script_buffer.rs @@ -83,7 +83,7 @@ impl ScriptBuffer { } pub fn write_assert(&mut self, cond: &str, error_msg: &str) { - self.writeln(&format!("ASSERT({}, \"Error: {}\");", cond, error_msg)); + self.writeln(&format!("ASSERT(({}), \"Error: {}\");", cond, error_msg)); } pub fn write_required_symbol(&mut self, name: &str) { diff --git a/slinky/src/traits.rs b/slinky/src/traits.rs index 81412f6..a18f56d 100644 --- a/slinky/src/traits.rs +++ b/slinky/src/traits.rs @@ -2,14 +2,16 @@ /* SPDX-License-Identifier: MIT */ use crate::{ - Document, EscapedPath, RequiredSymbol, Segment, Settings, SlinkyError, SymbolAssignment, + AssertEntry, Document, EscapedPath, RequiredSymbol, Segment, Settings, SlinkyError, + SymbolAssignment, }; mod private { use crate::{ - file_info::FileInfoSerial, gp_info::GpInfoSerial, required_symbol::RequiredSymbolSerial, - segment::SegmentSerial, symbol_assignment::SymbolAssignmentSerial, - vram_class::VramClassSerial, LinkerWriter, PartialLinkerWriter, + assert_entry::AssertEntrySerial, file_info::FileInfoSerial, gp_info::GpInfoSerial, + required_symbol::RequiredSymbolSerial, segment::SegmentSerial, + symbol_assignment::SymbolAssignmentSerial, vram_class::VramClassSerial, LinkerWriter, + PartialLinkerWriter, }; pub trait Sealed {} @@ -23,6 +25,7 @@ mod private { impl Sealed for VramClassSerial {} impl Sealed for SymbolAssignmentSerial {} impl Sealed for RequiredSymbolSerial {} + impl Sealed for AssertEntrySerial {} impl Sealed for Vec {} impl Sealed for Option {} @@ -39,6 +42,7 @@ pub trait ScriptImporter: private::Sealed { &mut self, required_symbols: &[RequiredSymbol], ) -> Result<(), SlinkyError>; + fn add_all_asserts(&mut self, asserts: &[AssertEntry]) -> Result<(), SlinkyError>; fn add_whole_document(&mut self, document: &Document) -> Result<(), SlinkyError> { self.add_all_segments(&document.segments)?; @@ -47,6 +51,7 @@ pub trait ScriptImporter: private::Sealed { } self.add_all_symbol_assignments(&document.symbol_assignments)?; self.add_all_required_symbols(&document.required_symbols)?; + self.add_all_asserts(&document.asserts)?; Ok(()) } diff --git a/tests/partial_linking/follow_segment.ld b/tests/partial_linking/follow_segment.ld index b80f768..b308f80 100644 --- a/tests/partial_linking/follow_segment.ld +++ b/tests/partial_linking/follow_segment.ld @@ -348,3 +348,5 @@ SECTIONS } ENTRY(ENTRYPOINT); + +ASSERT((main_VRAM_END <= 0x80400000), "Error: VRAM is larger than 4 MiB"); diff --git a/tests/partial_linking/follow_segment.yaml b/tests/partial_linking/follow_segment.yaml index 5a1f9a0..fd8af39 100644 --- a/tests/partial_linking/follow_segment.yaml +++ b/tests/partial_linking/follow_segment.yaml @@ -45,3 +45,7 @@ segments: - { path: asm/main/util.o } entry: ENTRYPOINT + +asserts: + - check: main_VRAM_END <= 0x80400000 + error_message: VRAM is larger than 4 MiB diff --git a/tests/partial_linking/required_syms.ld b/tests/partial_linking/required_syms.ld index f6fa916..837f9a0 100644 --- a/tests/partial_linking/required_syms.ld +++ b/tests/partial_linking/required_syms.ld @@ -86,6 +86,6 @@ SECTIONS } EXTERN(guMtxCatL); -ASSERT(DEFINED(guMtxCatL), "Error: Required symbol 'guMtxCatL' was not linked"); +ASSERT((DEFINED(guMtxCatL)), "Error: Required symbol 'guMtxCatL' was not linked"); EXTERN(__osSetCause); -ASSERT(DEFINED(__osSetCause), "Error: Required symbol '__osSetCause' was not linked"); +ASSERT((DEFINED(__osSetCause)), "Error: Required symbol '__osSetCause' was not linked"); diff --git a/tests/test_cases/archives.ld b/tests/test_cases/archives.ld index 25353f0..06ae941 100644 --- a/tests/test_cases/archives.ld +++ b/tests/test_cases/archives.ld @@ -164,3 +164,5 @@ SECTIONS *(*); } } + +ASSERT((boot_VRAM_END <= 0x80800000), "Error: VRAM is larger than 8 MiB"); diff --git a/tests/test_cases/archives.yaml b/tests/test_cases/archives.yaml index 15ba35f..2763753 100644 --- a/tests/test_cases/archives.yaml +++ b/tests/test_cases/archives.yaml @@ -17,3 +17,10 @@ segments: - { path: lib/libmus.a, subfile: aud_thread.o } - { path: lib/libmus.a, subfile: lib_memory.o } - { path: lib/libmus.a, subfile: aud_samples.o } + +asserts: + - check: boot_VRAM_END <= 0x80400000 + error_message: VRAM is larger than 4 MiB + include_if_any: [[ram_size, 4]] + - check: boot_VRAM_END <= 0x80800000 + error_message: VRAM is larger than 8 MiB diff --git a/tests/test_cases/drmario64.ld b/tests/test_cases/drmario64.ld index af5e115..7a64bfb 100644 --- a/tests/test_cases/drmario64.ld +++ b/tests/test_cases/drmario64.ld @@ -8095,3 +8095,5 @@ SECTIONS } ENTRY(entrypoint); + +ASSERT((boot_ROM_END <= 0x101000), "Error: boot segment is larger than 1 MiB"); diff --git a/tests/test_cases/drmario64.yaml b/tests/test_cases/drmario64.yaml index 9230dae..b1558fb 100644 --- a/tests/test_cases/drmario64.yaml +++ b/tests/test_cases/drmario64.yaml @@ -783,3 +783,7 @@ segments: - { path: src/assets/tutorial/tutorial_kasa.o } entry: entrypoint + +asserts: + - check: boot_ROM_END <= 0x101000 + error_message: boot segment is larger than 1 MiB diff --git a/tests/test_cases/required_syms.ld b/tests/test_cases/required_syms.ld index c301590..0ad5d3d 100644 --- a/tests/test_cases/required_syms.ld +++ b/tests/test_cases/required_syms.ld @@ -86,6 +86,6 @@ SECTIONS } EXTERN(guMtxCatL); -ASSERT(DEFINED(guMtxCatL), "Error: Required symbol 'guMtxCatL' was not linked"); +ASSERT((DEFINED(guMtxCatL)), "Error: Required symbol 'guMtxCatL' was not linked"); EXTERN(__osSetCause); -ASSERT(DEFINED(__osSetCause), "Error: Required symbol '__osSetCause' was not linked"); +ASSERT((DEFINED(__osSetCause)), "Error: Required symbol '__osSetCause' was not linked");