forked from libre-chip/cpu
wip parsing the xml
This commit is contained in:
parent
e4a7d9f59c
commit
8c02483b65
5 changed files with 967 additions and 0 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -279,6 +279,7 @@ dependencies = [
|
||||||
"fayalite",
|
"fayalite",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
"parse_powerisa_pdf",
|
"parse_powerisa_pdf",
|
||||||
|
"roxmltree",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"simple-mermaid",
|
"simple-mermaid",
|
||||||
|
|
@ -897,6 +898,15 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ base16ct = "1.0.0"
|
||||||
fayalite = { git = "https://git.libre-chip.org/libre-chip/fayalite.git", version = "0.3.0", branch = "master" }
|
fayalite = { git = "https://git.libre-chip.org/libre-chip/fayalite.git", version = "0.3.0", branch = "master" }
|
||||||
hex-literal = "1.1.0"
|
hex-literal = "1.1.0"
|
||||||
parse_powerisa_pdf = { git = "https://git.libre-chip.org/libre-chip/parse_powerisa_pdf.git", version = "0.1.0", branch = "master" }
|
parse_powerisa_pdf = { git = "https://git.libre-chip.org/libre-chip/parse_powerisa_pdf.git", version = "0.1.0", branch = "master" }
|
||||||
|
roxmltree = "0.21.1"
|
||||||
serde = { version = "1.0.202", features = ["derive"] }
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
simple-mermaid = "0.2.0"
|
simple-mermaid = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fayalite.workspace = true
|
fayalite.workspace = true
|
||||||
|
roxmltree.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
simple-mermaid.workspace = true
|
simple-mermaid.workspace = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod instruction;
|
pub mod instruction;
|
||||||
pub mod next_pc;
|
pub mod next_pc;
|
||||||
|
pub mod powerisa;
|
||||||
pub mod reg_alloc;
|
pub mod reg_alloc;
|
||||||
pub mod register;
|
pub mod register;
|
||||||
pub mod unit;
|
pub mod unit;
|
||||||
|
|
|
||||||
954
crates/cpu/src/powerisa.rs
Normal file
954
crates/cpu/src/powerisa.rs
Normal file
|
|
@ -0,0 +1,954 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
// See Notices.txt for copyright information
|
||||||
|
|
||||||
|
use roxmltree::{Attribute, Document, Node, NodeType};
|
||||||
|
use std::{fmt, panic::Location, sync::OnceLock};
|
||||||
|
|
||||||
|
const POWERISA_INSTRUCTIONS_XML: &str =
|
||||||
|
include_str!(concat!(env!("OUT_DIR"), "/powerisa-instructions.xml"));
|
||||||
|
|
||||||
|
enum Error<'a> {
|
||||||
|
XmlError(roxmltree::Error),
|
||||||
|
Unexpected {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
node: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
BodyTooShort {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
node: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
ExpectedTag {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
expected_tag_name: &'a str,
|
||||||
|
got_element: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
MissingAttribute {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
attribute_name: &'a str,
|
||||||
|
element: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
ExpectedAttribute {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
expected_attribute_name: &'a str,
|
||||||
|
attribute: Attribute<'a, 'static>,
|
||||||
|
element: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
UnexpectedAttribute {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
attribute: Attribute<'a, 'static>,
|
||||||
|
element: Node<'a, 'static>,
|
||||||
|
},
|
||||||
|
IsSubsetMustBeFalse {
|
||||||
|
loc: &'static Location<'static>,
|
||||||
|
is_subset: Attribute<'a, 'static>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<roxmltree::Error> for Error<'_> {
|
||||||
|
fn from(v: roxmltree::Error) -> Self {
|
||||||
|
Self::XmlError(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::XmlError(v) => v.fmt(f),
|
||||||
|
Error::Unexpected { loc, node } => write!(f, "at {loc}: unexpected node: {node:?}"),
|
||||||
|
Error::BodyTooShort { loc, node } => {
|
||||||
|
write!(f, "at {loc}: node's body is too short: {node:?}")
|
||||||
|
}
|
||||||
|
Error::ExpectedTag {
|
||||||
|
loc,
|
||||||
|
expected_tag_name,
|
||||||
|
got_element,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"at {loc}: expected tag {expected_tag_name:?} but got: {got_element:?}"
|
||||||
|
),
|
||||||
|
Error::MissingAttribute {
|
||||||
|
loc,
|
||||||
|
attribute_name,
|
||||||
|
element,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"at {loc}: missing attribute {attribute_name:?}: {element:?}"
|
||||||
|
),
|
||||||
|
Error::ExpectedAttribute {
|
||||||
|
loc,
|
||||||
|
expected_attribute_name,
|
||||||
|
attribute,
|
||||||
|
element,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"at {loc}: expected attribute with name {expected_attribute_name:?}: {attribute:?}\n\
|
||||||
|
in element: {element:?}"
|
||||||
|
),
|
||||||
|
Error::UnexpectedAttribute {
|
||||||
|
loc,
|
||||||
|
attribute,
|
||||||
|
element,
|
||||||
|
} => write!(
|
||||||
|
f,
|
||||||
|
"at {loc}: unexpected attribute: {attribute:?}\n\
|
||||||
|
in element: {element:?}"
|
||||||
|
),
|
||||||
|
Error::IsSubsetMustBeFalse { loc, is_subset } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"at {loc}: `is-subset` attribute must be `False`: {is_subset:?}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Instructions {
|
||||||
|
instructions: Box<[Instruction]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Instructions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("Instructions ")?;
|
||||||
|
self.instructions.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Parser<'a> {
|
||||||
|
parent: Node<'a, 'static>,
|
||||||
|
cur_node: Option<Node<'a, 'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Parser<'a> {
|
||||||
|
fn skip_comments(&mut self) {
|
||||||
|
while let Some(cur_node) = self.cur_node {
|
||||||
|
match cur_node.node_type() {
|
||||||
|
NodeType::Comment => {}
|
||||||
|
NodeType::Text | NodeType::Root | NodeType::Element | NodeType::PI => break,
|
||||||
|
}
|
||||||
|
self.cur_node = cur_node.next_sibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn skip_ws_and_comments(&mut self) {
|
||||||
|
while let Some(cur_node) = self.cur_node {
|
||||||
|
match cur_node.node_type() {
|
||||||
|
NodeType::Comment => {}
|
||||||
|
NodeType::Text => {
|
||||||
|
if cur_node
|
||||||
|
.text()
|
||||||
|
.is_some_and(|s| !s.trim_ascii_start().is_empty())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeType::Root | NodeType::Element | NodeType::PI => break,
|
||||||
|
}
|
||||||
|
self.cur_node = cur_node.next_sibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn element_body_todo(&mut self) {
|
||||||
|
self.cur_node = None;
|
||||||
|
}
|
||||||
|
fn peek<T: Parse>(&self) -> bool {
|
||||||
|
T::peek(self)
|
||||||
|
}
|
||||||
|
fn peek_any_element(&self) -> Option<Node<'a, 'static>> {
|
||||||
|
let mut parser = self.clone();
|
||||||
|
parser.skip_ws_and_comments();
|
||||||
|
let element = parser.cur_node?;
|
||||||
|
let NodeType::Element = element.node_type() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(element)
|
||||||
|
}
|
||||||
|
fn peek_element(&self, tag_name: &'a str) -> Option<Node<'a, 'static>> {
|
||||||
|
self.peek_any_element()
|
||||||
|
.filter(|element| element.has_tag_name(tag_name))
|
||||||
|
}
|
||||||
|
#[track_caller]
|
||||||
|
fn parse<T: Parse>(&mut self) -> Result<T, Error<'a>> {
|
||||||
|
T::parse(self)
|
||||||
|
}
|
||||||
|
#[track_caller]
|
||||||
|
fn parse_element<T, const N: usize>(
|
||||||
|
&mut self,
|
||||||
|
tag_name: &'a str,
|
||||||
|
attr_names: [&'a str; N],
|
||||||
|
f: impl FnOnce(
|
||||||
|
Node<'a, 'static>,
|
||||||
|
[Attribute<'a, 'static>; N],
|
||||||
|
&mut Parser<'a>,
|
||||||
|
) -> Result<T, Error<'a>>,
|
||||||
|
) -> Result<T, Error<'a>> {
|
||||||
|
self.parse_any_element(|element, parser| {
|
||||||
|
if !element.has_tag_name(tag_name) {
|
||||||
|
return Err(Error::ExpectedTag {
|
||||||
|
loc: Location::caller(),
|
||||||
|
expected_tag_name: tag_name,
|
||||||
|
got_element: element,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut attrs = [const { None }; N];
|
||||||
|
let mut attrs_iter = element.attributes();
|
||||||
|
for i in 0..N {
|
||||||
|
let Some(attr) = attrs_iter.next() else {
|
||||||
|
return Err(Error::MissingAttribute {
|
||||||
|
loc: Location::caller(),
|
||||||
|
attribute_name: attr_names[i],
|
||||||
|
element,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (attr.namespace(), attr.name()) != (None, attr_names[i]) {
|
||||||
|
return Err(Error::ExpectedAttribute {
|
||||||
|
loc: Location::caller(),
|
||||||
|
expected_attribute_name: attr_names[i],
|
||||||
|
attribute: attr,
|
||||||
|
element,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
attrs[i] = Some(attr);
|
||||||
|
}
|
||||||
|
if let Some(attribute) = attrs_iter.next() {
|
||||||
|
return Err(Error::UnexpectedAttribute {
|
||||||
|
loc: Location::caller(),
|
||||||
|
attribute,
|
||||||
|
element,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let attrs = attrs.map(|attr| attr.expect("filled in loop above"));
|
||||||
|
f(element, attrs, parser)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn parse_any_element<T>(
|
||||||
|
&mut self,
|
||||||
|
f: impl FnOnce(Node<'a, 'static>, &mut Parser<'a>) -> Result<T, Error<'a>>,
|
||||||
|
) -> Result<T, Error<'a>> {
|
||||||
|
self.skip_ws_and_comments();
|
||||||
|
let Some(element) = self.cur_node else {
|
||||||
|
return Err(Error::BodyTooShort {
|
||||||
|
loc: Location::caller(),
|
||||||
|
node: self.parent,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let NodeType::Element = element.node_type() else {
|
||||||
|
return Err(Error::Unexpected {
|
||||||
|
loc: Location::caller(),
|
||||||
|
node: element,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let mut parser = Parser {
|
||||||
|
parent: element,
|
||||||
|
cur_node: element.first_child(),
|
||||||
|
};
|
||||||
|
let retval = f(element, &mut parser)?;
|
||||||
|
parser.skip_ws_and_comments();
|
||||||
|
if let Some(node) = parser.cur_node {
|
||||||
|
Err(Error::Unexpected {
|
||||||
|
loc: Location::caller(),
|
||||||
|
node,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.cur_node = element.next_sibling();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn parse_document<T: Parse>(document: &'a Document<'static>) -> Result<T, Error<'a>> {
|
||||||
|
let parent = document.root();
|
||||||
|
let mut parser = Parser {
|
||||||
|
parent,
|
||||||
|
cur_node: parent.first_child(),
|
||||||
|
};
|
||||||
|
let retval = parser.parse()?;
|
||||||
|
parser.skip_ws_and_comments();
|
||||||
|
if let Some(node) = parser.cur_node {
|
||||||
|
Err(Error::Unexpected {
|
||||||
|
loc: Location::caller(),
|
||||||
|
node,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Parse: Sized {
|
||||||
|
fn peek<'a>(parser: &Parser<'a>) -> bool;
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Box<[T]> {
|
||||||
|
fn peek<'a>(_parser: &Parser<'a>) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
let mut retval = Vec::new();
|
||||||
|
while parser.peek::<T>() {
|
||||||
|
retval.push(parser.parse()?);
|
||||||
|
}
|
||||||
|
Ok(retval.into_boxed_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Parse> Parse for Option<T> {
|
||||||
|
fn peek<'a>(_parser: &Parser<'a>) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
parser.peek::<T>().then(|| parser.parse()).transpose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait ParseElementWithAttributes: Sized {
|
||||||
|
type Attributes<'a>: 'a;
|
||||||
|
|
||||||
|
fn parse_element_with_attributes<'a, T: ParseElement<AttributeNames = Self>>(
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<T, Error<'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> ParseElementWithAttributes for [&'static str; N] {
|
||||||
|
type Attributes<'a> = [Attribute<'a, 'static>; N];
|
||||||
|
|
||||||
|
fn parse_element_with_attributes<'a, T: ParseElement<AttributeNames = Self>>(
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<T, Error<'a>> {
|
||||||
|
parser.parse_element(T::TAG_NAME, T::ATTRIBUTE_NAMES, T::parse_element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElementWithAttributes for () {
|
||||||
|
type Attributes<'a> = ();
|
||||||
|
|
||||||
|
fn parse_element_with_attributes<'a, T: ParseElement<AttributeNames = Self>>(
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<T, Error<'a>> {
|
||||||
|
parser.parse_element(T::TAG_NAME, [], |element, [], parser| {
|
||||||
|
T::parse_element(element, (), parser)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait ParseElement: Parse {
|
||||||
|
type AttributeNames: ParseElementWithAttributes;
|
||||||
|
const TAG_NAME: &'static str;
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames;
|
||||||
|
fn parse_element<'a>(
|
||||||
|
element: Node<'a, 'static>,
|
||||||
|
attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ParseElement> Parse for T {
|
||||||
|
fn peek<'a>(parser: &Parser<'a>) -> bool {
|
||||||
|
parser.peek_element(Self::TAG_NAME).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
T::AttributeNames::parse_element_with_attributes::<T>(parser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Instructions {
|
||||||
|
pub fn instructions(&self) -> &[Instruction] {
|
||||||
|
&self.instructions
|
||||||
|
}
|
||||||
|
pub fn get() -> &'static Self {
|
||||||
|
static INSTRUCTIONS: OnceLock<Instructions> = OnceLock::new();
|
||||||
|
INSTRUCTIONS.get_or_init(|| {
|
||||||
|
let handle_error =
|
||||||
|
|e: Error<'_>| unreachable!("powerisa-instructions.xml failed to parse: {e}");
|
||||||
|
match Document::parse(POWERISA_INSTRUCTIONS_XML) {
|
||||||
|
Ok(document) => match Parser::parse_document(&document) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => handle_error(e),
|
||||||
|
},
|
||||||
|
Err(e) => handle_error(e.into()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for Instructions {
|
||||||
|
type AttributeNames = [&'static str; 1];
|
||||||
|
const TAG_NAME: &'static str = "instructions";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ["is-subset"];
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
let [is_subset] = attributes;
|
||||||
|
if is_subset.value() != "False" {
|
||||||
|
return Err(Error::IsSubsetMustBeFalse {
|
||||||
|
loc: Location::caller(),
|
||||||
|
is_subset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
instructions: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Instruction {
|
||||||
|
header: Box<[InstructionHeader]>,
|
||||||
|
code: Option<InstructionCode>,
|
||||||
|
description: Option<InstructionDescription>,
|
||||||
|
special_registers_altered: Option<InstructionSpecialRegistersAltered>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FlattenedOption<'a, T>(&'a Option<T>);
|
||||||
|
|
||||||
|
impl<T: fmt::Debug> fmt::Debug for FlattenedOption<'_, T> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
Some(v) => v.fmt(f),
|
||||||
|
None => f.write_str("None"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Instruction {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self {
|
||||||
|
header,
|
||||||
|
code,
|
||||||
|
description,
|
||||||
|
special_registers_altered,
|
||||||
|
} = self;
|
||||||
|
f.debug_struct("Instruction")
|
||||||
|
.field("header", header)
|
||||||
|
.field("code", &FlattenedOption(code))
|
||||||
|
.field("description", &FlattenedOption(description))
|
||||||
|
.field(
|
||||||
|
"special_registers_altered",
|
||||||
|
&FlattenedOption(special_registers_altered),
|
||||||
|
)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Instruction {
|
||||||
|
pub fn header(&self) -> &[InstructionHeader] {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
pub fn code(&self) -> Option<&InstructionCode> {
|
||||||
|
self.code.as_ref()
|
||||||
|
}
|
||||||
|
pub fn description(&self) -> Option<&InstructionDescription> {
|
||||||
|
self.description.as_ref()
|
||||||
|
}
|
||||||
|
pub fn special_registers_altered(&self) -> Option<&InstructionSpecialRegistersAltered> {
|
||||||
|
self.special_registers_altered.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for Instruction {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "instruction";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
header: parser.parse()?,
|
||||||
|
code: parser.parse()?,
|
||||||
|
description: parser.parse()?,
|
||||||
|
special_registers_altered: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InstructionHeader {
|
||||||
|
title: InstructionTitle,
|
||||||
|
mnemonics: InstructionMnemonics,
|
||||||
|
bit_fields: InstructionBitFields,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstructionHeader {
|
||||||
|
pub fn title(&self) -> &InstructionTitle {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
pub fn mnemonics(&self) -> &InstructionMnemonics {
|
||||||
|
&self.mnemonics
|
||||||
|
}
|
||||||
|
pub fn bit_fields(&self) -> &InstructionBitFields {
|
||||||
|
&self.bit_fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionHeader {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "header";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
title: parser.parse()?,
|
||||||
|
mnemonics: parser.parse()?,
|
||||||
|
bit_fields: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstructionTitle {
|
||||||
|
text_lines: TextLines,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for InstructionTitle {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { text_lines } = self;
|
||||||
|
text_lines.debug_fmt("InstructionTitle", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstructionTitle {
|
||||||
|
pub fn text_lines(&self) -> &TextLines {
|
||||||
|
&self.text_lines
|
||||||
|
}
|
||||||
|
pub fn lines(&self) -> &[TextLine] {
|
||||||
|
self.text_lines.lines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionTitle {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "title";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
text_lines: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstructionMnemonics {
|
||||||
|
text_lines: TextLines,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for InstructionMnemonics {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { text_lines } = self;
|
||||||
|
text_lines.debug_fmt("InstructionMnemonics", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstructionMnemonics {
|
||||||
|
pub fn text_lines(&self) -> &TextLines {
|
||||||
|
&self.text_lines
|
||||||
|
}
|
||||||
|
pub fn lines(&self) -> &[TextLine] {
|
||||||
|
self.text_lines.lines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionMnemonics {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "mnemonics";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
text_lines: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InstructionBitFields {}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionBitFields {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "bit-fields";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
// TODO
|
||||||
|
parser.element_body_todo();
|
||||||
|
Ok(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstructionCode {
|
||||||
|
text_lines: TextLines,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstructionCode {
|
||||||
|
pub fn text_lines(&self) -> &TextLines {
|
||||||
|
&self.text_lines
|
||||||
|
}
|
||||||
|
pub fn lines(&self) -> &[TextLine] {
|
||||||
|
self.text_lines.lines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for InstructionCode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { text_lines } = self;
|
||||||
|
text_lines.debug_fmt("InstructionCode", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionCode {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "code";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
text_lines: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InstructionDescription {
|
||||||
|
text_lines: TextLines,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstructionDescription {
|
||||||
|
pub fn text_lines(&self) -> &TextLines {
|
||||||
|
&self.text_lines
|
||||||
|
}
|
||||||
|
pub fn lines(&self) -> &[TextLine] {
|
||||||
|
self.text_lines.lines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for InstructionDescription {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { text_lines } = self;
|
||||||
|
text_lines.debug_fmt("InstructionDescription", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionDescription {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "description";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
Ok(Self {
|
||||||
|
text_lines: parser.parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InstructionSpecialRegistersAltered {}
|
||||||
|
|
||||||
|
impl ParseElement for InstructionSpecialRegistersAltered {
|
||||||
|
type AttributeNames = ();
|
||||||
|
const TAG_NAME: &'static str = "special-registers-altered";
|
||||||
|
const ATTRIBUTE_NAMES: Self::AttributeNames = ();
|
||||||
|
|
||||||
|
fn parse_element<'a>(
|
||||||
|
_element: Node<'a, 'static>,
|
||||||
|
_attributes: <Self::AttributeNames as ParseElementWithAttributes>::Attributes<'a>,
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
// TODO
|
||||||
|
parser.element_body_todo();
|
||||||
|
Ok(Self {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TextLineItem {
|
||||||
|
Text(Box<str>),
|
||||||
|
Code(Box<[TextLineItem]>),
|
||||||
|
Bold(Box<[TextLineItem]>),
|
||||||
|
Italic(Box<[TextLineItem]>),
|
||||||
|
Subscript(Box<[TextLineItem]>),
|
||||||
|
Superscript(Box<[TextLineItem]>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TextLineItem {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Text(v) => v.fmt(f),
|
||||||
|
Self::Code(v) => {
|
||||||
|
f.write_str("Code")?;
|
||||||
|
v.fmt(f)
|
||||||
|
}
|
||||||
|
Self::Bold(v) => {
|
||||||
|
f.write_str("Bold")?;
|
||||||
|
v.fmt(f)
|
||||||
|
}
|
||||||
|
Self::Italic(v) => {
|
||||||
|
f.write_str("Italic")?;
|
||||||
|
v.fmt(f)
|
||||||
|
}
|
||||||
|
Self::Subscript(v) => {
|
||||||
|
f.write_str("Subscript")?;
|
||||||
|
v.fmt(f)
|
||||||
|
}
|
||||||
|
Self::Superscript(v) => {
|
||||||
|
f.write_str("Superscript")?;
|
||||||
|
v.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait TextLineItemMatch<'a>: Sized {
|
||||||
|
type Output;
|
||||||
|
fn text(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn code(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn bold(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn italic(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn subscript(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn superscript(self, node: Node<'a, 'static>) -> Self::Output;
|
||||||
|
fn match_node(self, node: Node<'a, 'static>) -> Option<Self::Output> {
|
||||||
|
match node.node_type() {
|
||||||
|
NodeType::Element => {
|
||||||
|
if node.tag_name().namespace().is_none() {
|
||||||
|
Some(match node.tag_name().name() {
|
||||||
|
"code" => self.code(node),
|
||||||
|
"b" => self.bold(node),
|
||||||
|
"i" => self.italic(node),
|
||||||
|
"sub" => self.subscript(node),
|
||||||
|
"sup" => self.superscript(node),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeType::Root | NodeType::PI | NodeType::Comment => None,
|
||||||
|
NodeType::Text => Some(self.text(node)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for TextLineItem {
|
||||||
|
fn peek<'a>(parser: &Parser<'a>) -> bool {
|
||||||
|
let mut parser = parser.clone();
|
||||||
|
parser.skip_comments();
|
||||||
|
struct PeekMatch;
|
||||||
|
impl<'a> TextLineItemMatch<'a> for PeekMatch {
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
fn text(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
fn code(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
fn bold(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
fn italic(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
fn subscript(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
fn superscript(self, _node: Node<'a, 'static>) -> Self::Output {}
|
||||||
|
}
|
||||||
|
parser
|
||||||
|
.cur_node
|
||||||
|
.is_some_and(|node| PeekMatch.match_node(node).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
parser.skip_comments();
|
||||||
|
struct ParseMatch<'b, 'a>(&'b mut Parser<'a>);
|
||||||
|
impl<'a> TextLineItemMatch<'a> for ParseMatch<'_, 'a> {
|
||||||
|
type Output = Result<TextLineItem, Error<'a>>;
|
||||||
|
|
||||||
|
fn text(self, node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
self.0.cur_node = node.next_sibling();
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(TextLineItem::Text(node.text().unwrap_or("").into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code(self, _node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
let retval = self.0.parse_element("code", [], |_node, [], parser| {
|
||||||
|
Ok(TextLineItem::Code(TextLine::parse(parser)?.items))
|
||||||
|
})?;
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bold(self, _node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
let retval = self.0.parse_element("b", [], |_node, [], parser| {
|
||||||
|
Ok(TextLineItem::Bold(TextLine::parse(parser)?.items))
|
||||||
|
})?;
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn italic(self, _node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
let retval = self.0.parse_element("i", [], |_node, [], parser| {
|
||||||
|
Ok(TextLineItem::Italic(TextLine::parse(parser)?.items))
|
||||||
|
})?;
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subscript(self, _node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
let retval = self.0.parse_element("sub", [], |_node, [], parser| {
|
||||||
|
Ok(TextLineItem::Subscript(TextLine::parse(parser)?.items))
|
||||||
|
})?;
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn superscript(self, _node: Node<'a, 'static>) -> Self::Output {
|
||||||
|
let retval = self.0.parse_element("sup", [], |_node, [], parser| {
|
||||||
|
Ok(TextLineItem::Superscript(TextLine::parse(parser)?.items))
|
||||||
|
})?;
|
||||||
|
self.0.skip_comments();
|
||||||
|
Ok(retval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(item) = parser
|
||||||
|
.cur_node
|
||||||
|
.and_then(|node| ParseMatch(parser).match_node(node))
|
||||||
|
.transpose()?
|
||||||
|
else {
|
||||||
|
return Err(Error::BodyTooShort {
|
||||||
|
loc: Location::caller(),
|
||||||
|
node: parser.parent,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Ok(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextLine {
|
||||||
|
items: Box<[TextLineItem]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TextLine {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { items } = self;
|
||||||
|
f.write_str("TextLine ")?;
|
||||||
|
items.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLine {
|
||||||
|
pub fn items(&self) -> &[TextLineItem] {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
fn parse_with_options<'a>(
|
||||||
|
parser: &mut Parser<'a>,
|
||||||
|
remove_leading_nl: bool,
|
||||||
|
) -> Result<Self, Error<'a>> {
|
||||||
|
parser.skip_comments();
|
||||||
|
let mut items = Vec::new();
|
||||||
|
if let Some(node) = parser.cur_node {
|
||||||
|
if node.is_text() {
|
||||||
|
let mut text = node.text().expect("known to be text");
|
||||||
|
if remove_leading_nl {
|
||||||
|
text = text
|
||||||
|
.strip_prefix("\r\n")
|
||||||
|
.or_else(|| text.strip_prefix(&['\r', '\n']))
|
||||||
|
.unwrap_or(text);
|
||||||
|
}
|
||||||
|
if !text.is_empty() {
|
||||||
|
items.push(TextLineItem::Text(text.into()));
|
||||||
|
}
|
||||||
|
parser.cur_node = node.next_sibling();
|
||||||
|
parser.skip_comments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while TextLineItem::peek(parser) {
|
||||||
|
items.push(TextLineItem::parse(parser)?);
|
||||||
|
parser.skip_comments();
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
items: items.into_boxed_slice(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for TextLine {
|
||||||
|
fn peek<'a>(_parser: &Parser<'a>) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
Self::parse_with_options(parser, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TextLines {
|
||||||
|
lines: Box<[TextLine]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for TextLines {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.debug_fmt("TextLines", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextLines {
|
||||||
|
pub fn lines(&self) -> &[TextLine] {
|
||||||
|
&self.lines
|
||||||
|
}
|
||||||
|
fn debug_fmt(&self, name: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let Self { lines } = self;
|
||||||
|
f.write_str(name)?;
|
||||||
|
fmt::Debug::fmt(lines, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for TextLines {
|
||||||
|
fn peek<'a>(parser: &Parser<'a>) -> bool {
|
||||||
|
parser.peek_element("br").is_some() || TextLine::peek(parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<'a>(parser: &mut Parser<'a>) -> Result<Self, Error<'a>> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
lines.push(TextLine::parse(parser)?);
|
||||||
|
while parser.peek_element("br").is_some() {
|
||||||
|
parser.parse_element("br", [], |_element, [], _parser| Ok(()))?;
|
||||||
|
lines.push(TextLine::parse_with_options(parser, true)?);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
lines: lines.into_boxed_slice(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn test_instructions_parses() {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let instructions = Instructions::get();
|
||||||
|
let mut written = String::new();
|
||||||
|
for (i, instruction) in instructions.instructions().iter().enumerate() {
|
||||||
|
written.clear();
|
||||||
|
write!(written, "{instruction:#?}").expect("known to not error");
|
||||||
|
println!("------\n{written}\n------");
|
||||||
|
let expected: &str = match i {
|
||||||
|
#[cfg(todo)]
|
||||||
|
0 => "",
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
assert!(written == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue