logicaffeine_web/ui/pages/
hardware_spec.rs1pub const PIGEONS: &str = "pigeons";
7pub const REGISTERS: &str = "registers";
9pub const FIELDS: [&str; 2] = [PIGEONS, REGISTERS];
11pub const PIGEONS_RANGE: std::ops::RangeInclusive<usize> = 2..=20;
14
15pub fn opens_comment(after: Option<char>) -> bool {
18 after.map_or(true, char::is_whitespace)
19}
20
21pub fn comment_start(line: &str) -> Option<usize> {
23 let bytes = line.as_bytes();
24 let mut i = 0;
25 while i < bytes.len() {
26 if bytes[i] == b'#' {
27 let run_end = i + bytes[i..].iter().take_while(|&&b| b == b'#').count();
28 if opens_comment(line[run_end..].chars().next()) {
29 return Some(i);
30 }
31 i = run_end;
32 } else {
33 i += 1;
34 }
35 }
36 None
37}
38
39pub fn content(line: &str) -> &str {
41 line[..comment_start(line).unwrap_or(line.len())].trim()
42}
43
44pub fn is_field(word: &str, field: &str) -> bool {
46 let mut chars = field.chars();
47 let capitalised = chars.next().map(|first| first.to_uppercase().chain(chars).collect::<String>());
48 word == field || capitalised.is_some_and(|c| word == c)
49}
50
51pub fn field_value<'a>(line: &'a str, field: &str) -> Option<&'a str> {
54 let (name, value) = content(line).split_once(':')?;
55 is_field(name, field).then(|| value.trim())
56}
57
58pub fn hover(source: &str, offset: usize) -> Option<String> {
61 let kind = kind(source)?;
62 let (line_at, line) = lines(source).find(|(at, line)| (*at..=at + line.len()).contains(&offset))?;
63 let entry = entry(line)?;
64 let at = offset - line_at;
65 let on_name = (entry.name_at..entry.name_at + entry.name.len()).contains(&at);
66 let on_value = (entry.value_at..entry.value_at + entry.value.len()).contains(&at);
67 if !on_name && !on_value {
68 return None;
69 }
70 if is_field(entry.name, PIGEONS) {
71 let (low, high) = (PIGEONS_RANGE.start(), PIGEONS_RANGE.end());
72 return Some(format!(
73 "**pigeons**: how many pigeons fly into one fewer holes, from {low} to {high}. They can never all fit, and the prover certifies why, with no Z3."
74 ));
75 }
76 if is_field(entry.name, REGISTERS) {
77 return Some(
78 "**registers**: the physical register budget. Every other line is a variable's live range, `name: first-last`, and the allocator certifies whether they fit.".to_string(),
79 );
80 }
81 match (kind, live_range(entry.value)) {
82 (Kind::Registers, Some((first, last))) => Some(format!("**{}** is live from instruction {first} to {last}.", entry.name)),
83 _ => None,
84 }
85}
86
87pub fn problems(source: &str) -> Vec<crate::ui::components::problems::Problem> {
90 use crate::ui::components::problems::{Problem, Severity};
91 let Some(kind) = kind(source) else { return Vec::new() };
92 let problem = |start: usize, end: usize, message: String| Problem {
93 start,
94 end,
95 severity: Severity::Error,
96 unnecessary: false,
97 message,
98 source: Some("spec".to_string()),
99 code: None,
100 docs: None,
101 };
102 let mut out = Vec::new();
103 for (line_at, line) in lines(source) {
104 let body = content(line);
105 if body.is_empty() {
106 continue;
107 }
108 let entry = entry(line);
109 let value_span = |e: &Entry| {
111 if e.value.is_empty() {
112 (line_at + e.name_at, line_at + e.name_at + e.name.len())
113 } else {
114 (line_at + e.value_at, line_at + e.value_at + e.value.len())
115 }
116 };
117 match (kind, entry) {
118 (Kind::Pigeons, Some(e)) if is_field(e.name, PIGEONS) => {
119 let (start, end) = value_span(&e);
120 let (low, high) = (PIGEONS_RANGE.start(), PIGEONS_RANGE.end());
121 match e.value.parse::<usize>() {
122 Err(_) => out.push(problem(start, end, format!("`pigeons` takes a whole number of pigeons, from {low} to {high}."))),
123 Ok(n) if !PIGEONS_RANGE.contains(&n) => {
124 out.push(problem(start, end, format!("`pigeons` takes from {low} to {high} pigeons; {n} is out of range.")))
125 }
126 Ok(_) => {}
127 }
128 }
129 (Kind::Pigeons, _) => {
130 let start = line_at + (line.len() - line.trim_start().len());
131 out.push(problem(start, start + body.len(), "A pigeonhole spec holds one line, `pigeons: N`; this line stops it being read as one.".to_string()));
132 }
133 (Kind::Registers, Some(e)) if is_field(e.name, REGISTERS) => {
134 if e.value.parse::<usize>().is_err() {
135 let (start, end) = value_span(&e);
136 out.push(problem(start, end, "`registers` takes a whole number of registers.".to_string()));
137 }
138 }
139 (Kind::Registers, Some(e)) => {
140 let (start, end) = value_span(&e);
141 match live_range(e.value) {
142 None => out.push(problem(start, end, format!("`{}` needs a live range `first-last`, like `0-3`.", e.name))),
143 Some((first, last)) if first >= last => out.push(problem(
144 start,
145 end,
146 format!("`{}`'s live range must end after it starts; `{}` does not, so the allocator skips it.", e.name, e.value),
147 )),
148 Some(_) => {}
149 }
150 }
151 (Kind::Registers, None) => {}
152 }
153 }
154 out
155}
156
157#[derive(Clone, Copy)]
159enum Kind {
160 Pigeons,
161 Registers,
162}
163
164fn kind(source: &str) -> Option<Kind> {
165 let names = || lines(source).filter_map(|(_, line)| entry(line)).map(|e| e.name);
166 if names().any(|name| is_field(name, PIGEONS)) {
167 Some(Kind::Pigeons)
168 } else if names().any(|name| is_field(name, REGISTERS)) {
169 Some(Kind::Registers)
170 } else {
171 None
172 }
173}
174
175fn lines(source: &str) -> impl Iterator<Item = (usize, &str)> {
177 let mut at = 0;
178 source.split_inclusive('\n').map(move |raw| {
179 let here = at;
180 at += raw.len();
181 (here, raw.trim_end_matches(['\n', '\r']))
182 })
183}
184
185struct Entry<'a> {
188 name: &'a str,
189 name_at: usize,
190 value: &'a str,
191 value_at: usize,
192}
193
194fn entry(line: &str) -> Option<Entry<'_>> {
195 let body = &line[..comment_start(line).unwrap_or(line.len())];
196 let colon = body.find(':')?;
197 let before = &body[..colon];
198 let name = before.trim();
199 if name.is_empty() {
200 return None;
201 }
202 let after = &body[colon + 1..];
203 Some(Entry {
204 name,
205 name_at: before.len() - before.trim_start().len(),
206 value: after.trim(),
207 value_at: colon + 1 + (after.len() - after.trim_start().len()),
208 })
209}
210
211fn live_range(value: &str) -> Option<(i64, i64)> {
213 let (first, last) = value.split_once('-')?;
214 Some((first.trim().parse().ok()?, last.trim().parse().ok()?))
215}
216
217#[cfg(test)]
218#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/pages/hardware_spec/service_tests.rs"]
219mod service_tests;
220
221#[cfg(test)]
222#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/pages/hardware_spec/tests.rs"]
223mod tests;