1#![allow(clippy::result_large_err)]
3use std::collections::BTreeMap;
8
9use thiserror::Error;
10
11use kiln_build::{BuildDiagnostic, Severity, SourceSet};
12use kiln_core::{LintSeverity, ResolvedConfig};
13use slang_rs::{CompileRequest, Severity as SlangSeverity, Slang, SlangError};
14
15#[derive(Debug, Error)]
16pub enum LintError {
17 #[error(transparent)]
18 Slang(#[from] SlangError),
19}
20
21pub fn check(
24 slang: &Slang,
25 resolved: &ResolvedConfig,
26 source_set: &SourceSet,
27) -> Result<Vec<BuildDiagnostic>, LintError> {
28 let req = build_request(resolved, source_set);
29 let result = slang.compile(&req)?;
30 let diagnostics = result
31 .diagnostics
32 .into_iter()
33 .filter_map(|d| convert(d, &resolved.lint.rules))
34 .collect();
35 Ok(diagnostics)
36}
37
38pub(crate) fn build_request(resolved: &ResolvedConfig, source_set: &SourceSet) -> CompileRequest {
40 use kiln_core::SvLanguage;
41
42 let mut req = CompileRequest::builder().top(&resolved.design.top);
43 for aux in &resolved.design.aux_tops {
47 req = req.extra_arg("--top".to_string());
48 req = req.extra_arg(aux.clone());
49 }
50 for s in source_set.files() {
51 req = req.source(s.clone());
52 }
53 for d in &resolved.design.include_dirs {
54 req = req.include_dir(source_set.project_root.join(d));
55 }
56 for (k, v) in &resolved.design.defines {
57 req = req.define(k.clone(), v.clone());
58 }
59 if let Some(ts) = &resolved.design.timescale {
60 req = req.extra_arg("--timescale".to_string());
61 req = req.extra_arg(ts.clone());
62 }
63 if let Some(lang) = resolved.design.language {
64 let flag = match lang {
65 SvLanguage::Sv2005 => "1364-2005",
66 SvLanguage::Sv2009 => "1800-2009",
67 SvLanguage::Sv2012 => "1800-2012",
68 SvLanguage::Sv2017 => "1800-2017",
69 SvLanguage::Sv2023 => "1800-2023",
70 };
71 req = req.extra_arg("--std".to_string());
72 req = req.extra_arg(flag.to_string());
73 }
74 for lib in &resolved.design.libraries {
75 req = req.extra_arg("-y".to_string());
76 req = req.extra_arg(lib.clone());
77 }
78 for (id, sev) in &resolved.lint.rules {
80 if matches!(sev, LintSeverity::Error | LintSeverity::Warn) {
81 req = req.extra_arg(format!("-W{id}"));
82 }
83 }
84 for arg in &resolved.tool_slang.extra_args {
85 req = req.extra_arg(arg.clone());
86 }
87 req.build()
91}
92
93fn convert(
94 d: slang_rs::Diagnostic,
95 rules: &BTreeMap<String, LintSeverity>,
96) -> Option<BuildDiagnostic> {
97 let mut severity = match d.severity {
98 SlangSeverity::Error => Severity::Error,
99 SlangSeverity::Warning => Severity::Warning,
100 SlangSeverity::Note => Severity::Note,
101 };
102 if let Some(name) = &d.option_name {
104 if let Some(over) = rules.get(name) {
105 match over {
106 LintSeverity::Error => severity = Severity::Error,
107 LintSeverity::Warn => severity = Severity::Warning,
108 LintSeverity::Off | LintSeverity::Deny => return None,
109 }
110 }
111 }
112 let (file, line, column) = match d.location {
113 Some(loc) => (Some(loc.file), Some(loc.line), Some(loc.column)),
114 None => (None, None, None),
115 };
116 Some(BuildDiagnostic {
117 severity,
118 code: d.option_name,
119 file,
120 line,
121 column,
122 message: d.message,
123 })
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use slang_rs::{Diagnostic as SlangDiag, Location};
130
131 fn diag(option: Option<&str>, sev: SlangSeverity) -> SlangDiag {
132 SlangDiag {
133 severity: sev,
134 message: "msg".to_string(),
135 option_name: option.map(String::from),
136 location: Some(Location {
137 file: "f.sv".into(),
138 line: 1,
139 column: 1,
140 }),
141 symbol_path: None,
142 }
143 }
144
145 #[test]
146 fn convert_no_rule_passes_severity_through() {
147 let rules = BTreeMap::new();
148 let d = convert(diag(Some("foo"), SlangSeverity::Warning), &rules).unwrap();
149 assert_eq!(d.severity, Severity::Warning);
150 assert_eq!(d.code.as_deref(), Some("foo"));
151 }
152
153 #[test]
154 fn convert_error_override_promotes_warning() {
155 let mut rules = BTreeMap::new();
156 rules.insert("width-trunc".to_string(), LintSeverity::Error);
157 let d = convert(diag(Some("width-trunc"), SlangSeverity::Warning), &rules).unwrap();
158 assert_eq!(d.severity, Severity::Error);
159 }
160
161 #[test]
162 fn convert_warn_override_demotes_error() {
163 let mut rules = BTreeMap::new();
164 rules.insert("foo".to_string(), LintSeverity::Warn);
165 let d = convert(diag(Some("foo"), SlangSeverity::Error), &rules).unwrap();
166 assert_eq!(d.severity, Severity::Warning);
167 }
168
169 #[test]
170 fn convert_off_drops_diagnostic() {
171 let mut rules = BTreeMap::new();
172 rules.insert("foo".to_string(), LintSeverity::Off);
173 assert!(convert(diag(Some("foo"), SlangSeverity::Warning), &rules).is_none());
174 }
175
176 #[test]
177 fn convert_without_option_name_uses_native_severity() {
178 let rules = BTreeMap::new();
179 let d = convert(diag(None, SlangSeverity::Error), &rules).unwrap();
180 assert_eq!(d.severity, Severity::Error);
181 assert_eq!(d.code, None);
182 }
183
184 #[test]
185 fn lint_config_round_trips_in_manifest() {
186 use kiln_core::Manifest;
187 let m: Manifest = r#"
188 [package]
189 name = "p"
190 version = "0.1.0"
191
192 [design]
193 top = "t"
194
195 [lint]
196 width-trunc = "error"
197 unused-net = "warn"
198 implicit-net = "off"
199 "#
200 .parse()
201 .unwrap();
202 assert_eq!(m.lint.rules.len(), 3);
203 assert_eq!(m.lint.rules.get("width-trunc"), Some(&LintSeverity::Error));
204 assert_eq!(m.lint.rules.get("implicit-net"), Some(&LintSeverity::Off));
205 }
206
207 fn resolved(manifest_str: &str) -> (kiln_core::ResolvedConfig, kiln_build::SourceSet) {
208 let m: kiln_core::Manifest = manifest_str.parse().unwrap();
209 let resolved = kiln_core::ResolvedConfig::resolve(&m, "dev");
210 let ss = kiln_build::SourceSet {
211 project_root: std::path::PathBuf::from("/p"),
212 files: vec![],
213 };
214 (resolved, ss)
215 }
216
217 #[test]
218 fn timescale_in_design_reaches_slang_args() {
219 let (r, ss) = resolved(
220 r#"
221 [package]
222 name = "p"
223 version = "0.1.0"
224 [design]
225 top = "t"
226 timescale = "1ns/1ps"
227 "#,
228 );
229 let req = build_request(&r, &ss);
230 let args = req.extra_args();
231 let pos = args.iter().position(|a| a == "--timescale");
232 assert!(
233 pos.is_some(),
234 "--timescale not found in slang args: {args:?}"
235 );
236 assert_eq!(args[pos.unwrap() + 1], "1ns/1ps");
237 }
238
239 #[test]
240 fn language_in_design_reaches_slang_std_arg() {
241 let (r, ss) = resolved(
242 r#"
243 [package]
244 name = "p"
245 version = "0.1.0"
246 [design]
247 top = "t"
248 language = "sv2017"
249 "#,
250 );
251 let req = build_request(&r, &ss);
252 let args = req.extra_args();
253 let pos = args.iter().position(|a| a == "--std");
254 assert!(pos.is_some(), "--std not found in slang args: {args:?}");
255 assert_eq!(args[pos.unwrap() + 1], "1800-2017");
256 }
257
258 #[test]
259 fn libraries_in_design_reach_slang_y_flag() {
260 let (r, ss) = resolved(
261 r#"
262 [package]
263 name = "p"
264 version = "0.1.0"
265 [design]
266 top = "t"
267 libraries = ["vendor/lib"]
268 "#,
269 );
270 let req = build_request(&r, &ss);
271 let args = req.extra_args();
272 let pos = args.iter().position(|a| a == "-y");
273 assert!(pos.is_some(), "-y not found in slang args: {args:?}");
274 assert_eq!(args[pos.unwrap() + 1], "vendor/lib");
275 }
276
277 #[test]
278 fn aux_tops_become_additional_top_flags() {
279 let (r, ss) = resolved(
280 r#"
281 [package]
282 name = "p"
283 version = "0.1.0"
284 [design]
285 top = "z1top"
286 aux_tops = ["glbl", "BUFG_helper"]
287 "#,
288 );
289 let req = build_request(&r, &ss);
290 let args = req.extra_args();
291 let positions: Vec<usize> = args
293 .iter()
294 .enumerate()
295 .filter(|(_, a)| a.as_str() == "--top")
296 .map(|(i, _)| i)
297 .collect();
298 assert_eq!(positions.len(), 2, "expected 2 extra `--top`: {args:?}");
299 assert_eq!(args[positions[0] + 1], "glbl");
300 assert_eq!(args[positions[1] + 1], "BUFG_helper");
301 }
302
303 #[test]
304 fn tool_slang_extra_args_appended_last() {
305 let (r, ss) = resolved(
306 r#"
307 [package]
308 name = "p"
309 version = "0.1.0"
310 [design]
311 top = "t"
312 timescale = "1ns/1ps"
313 [tool.slang]
314 extra_args = ["--allow-hierarchical-const"]
315 "#,
316 );
317 let req = build_request(&r, &ss);
318 let args = req.extra_args();
319 let ts_pos = args.iter().position(|a| a == "--timescale").unwrap();
321 let extra_pos = args
322 .iter()
323 .position(|a| a == "--allow-hierarchical-const")
324 .unwrap();
325 assert!(
326 extra_pos > ts_pos,
327 "extra_args should come after design args"
328 );
329 }
330}