1mod monaco;
2
3use std::{
4 collections::BTreeMap,
5 path::PathBuf,
6 sync::{Arc, RwLock},
7};
8
9use axum::{
10 extract::State,
11 http::{header, StatusCode, Uri},
12 response::{IntoResponse, Response},
13 routing::get,
14 Json, Router,
15};
16use clap::Parser;
17use color_eyre::eyre::Context as _;
18use itertools::Itertools;
19use rust_embed::Embed;
20use serde::{Deserialize, Serialize};
21use slang::{Position, SourceFile, Span};
22use tapi::endpoints::RouterExt;
23use tracing_subscriber::prelude::*;
24
25pub type Result<T, E = color_eyre::eyre::Error> = std::result::Result<T, E>;
26
27pub use color_eyre::eyre::{bail, eyre};
28
29pub mod prelude {
30 pub use super::Result;
31 pub use color_eyre::eyre::{bail, eyre};
32 pub use miette;
33 pub use slang;
34 pub use smtlib::{self, prelude::*};
35 pub use tracing;
36}
37
38#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub enum Severity {
41 Info,
42 Warning,
43 Error,
44}
45
46#[non_exhaustive]
47#[derive(Debug, Clone, PartialEq)]
48pub struct Report {
49 pub severity: Severity,
50 pub span: Span,
51 pub message: String,
52}
53
54#[non_exhaustive]
55#[derive(Debug, Clone, Serialize, tapi::Tapi)]
56pub enum Color {
57 Red,
58 Green,
59 Blue,
60}
61
62pub struct Context {
63 storage: smtlib::Storage,
64 reports: RwLock<Vec<Report>>,
65 message: RwLock<Option<(String, Color)>>,
66}
67
68struct Logger {}
69
70impl smtlib::Logger for Logger {
71 fn exec(&self, cmd: smtlib::lowlevel::ast::Command) {
72 let span = tracing::span!(tracing::Level::INFO, "smt");
73 let _enter = span.enter();
74 tracing::info!("> {cmd}")
75 }
76
77 fn response(&self, _cmd: smtlib::lowlevel::ast::Command, res: &str) {
78 let span = tracing::span!(tracing::Level::INFO, "smt");
79 let _enter = span.enter();
80 tracing::info!("< {}", res.trim())
81 }
82}
83
84impl Context {
85 fn new(_file: &SourceFile) -> Context {
86 Context {
87 storage: smtlib::Storage::new(),
88 reports: Default::default(),
89 message: Default::default(),
90 }
91 }
92
93 pub fn smt_st(&self) -> &smtlib::Storage {
94 &self.storage
95 }
96
97 pub fn solver(&self) -> Result<smtlib::Solver<smtlib::backend::z3_binary::Z3Binary>> {
98 let mut solver = smtlib::Solver::new(
99 self.smt_st(),
100 smtlib::backend::z3_binary::Z3Binary::new("z3")
101 .context("failed to find `z3`. is it installed and in your path?")?,
102 )?;
103 solver.set_logger(Logger {});
104 solver.set_timeout(2_000)?;
105 Ok(solver)
106 }
107
108 pub fn report(&self, severity: Severity, span: Span, message: impl std::fmt::Display) {
109 let message = message.to_string();
110 self.reports.write().unwrap().push(Report {
111 severity,
112 span,
113 message,
114 })
115 }
116 pub fn info(&self, span: Span, message: impl std::fmt::Display) {
117 self.report(Severity::Info, span, message)
118 }
119 pub fn warning(&self, span: Span, message: impl std::fmt::Display) {
120 self.report(Severity::Warning, span, message)
121 }
122 pub fn error(&self, span: Span, message: impl std::fmt::Display) {
123 self.report(Severity::Error, span, message)
124 }
125
126 pub fn set_message(&self, message: impl std::fmt::Display, color: Color) {
127 *self.message.write().unwrap() = Some((message.to_string(), color))
128 }
129
130 pub fn reports(&self) -> Vec<Report> {
131 self.reports.read().unwrap().clone()
132 }
133
134 #[track_caller]
135 pub fn todo(&self, span: Span) {
136 let msg = format!("not yet implemented: {}", std::panic::Location::caller());
137 tracing::error!("{msg}");
138 self.warning(span, msg);
139 }
140}
141
142pub struct HoverResponse {
143 pub span: Span,
144 pub contents: Vec<String>,
145}
146
147pub trait Hook {
148 fn analyze(&self, cx: &Context, file: &SourceFile) -> Result<()>;
149 #[allow(unused_variables)]
150 fn hover(&self, cx: &Context, file: &SourceFile, pos: Position) -> Option<HoverResponse> {
151 None
152 }
153}
154
155#[derive(Embed)]
156#[folder = "static/"]
157struct Asset;
158
159pub async fn run(hook: impl Hook + Send + Sync + 'static) {
160 let _ = color_eyre::install();
161
162 tracing_subscriber::Registry::default()
163 .with(tracing_error::ErrorLayer::default())
164 .with(
165 tracing_subscriber::EnvFilter::builder()
166 .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
167 .from_env_lossy(),
168 )
169 .with(
170 tracing_subscriber::fmt::layer()
171 .with_target(false)
172 .without_time(),
173 )
174 .with(tracing_subscriber::filter::FilterFn::new(|m| {
175 !m.target().contains("hyper")
176 }))
177 .init();
178
179 match run_impl(Arc::new(hook)).await {
180 Ok(()) => {}
181 Err(err) => println!("{err:?}"),
182 }
183}
184
185pub struct TestResult {
186 reports: Vec<Report>,
187 error: Option<color_eyre::eyre::Error>,
188}
189
190pub fn test(hook: impl Hook + 'static, src: &str) -> TestResult {
191 match run_hook(&hook, src) {
192 Ok(reports) => TestResult {
193 reports,
194 error: None,
195 },
196 Err((reports, error)) => TestResult {
197 reports,
198 error: Some(error),
199 },
200 }
201}
202
203impl TestResult {
204 pub fn has_message(&self, message: &str) -> bool {
205 self.reports.iter().any(|r| r.message == message)
206 }
207 pub fn has_errors(&self) -> bool {
208 self.error.is_some() || self.reports.iter().any(|r| r.severity == Severity::Error)
209 }
210 pub fn error(&self) -> Option<&color_eyre::eyre::Error> {
211 self.error.as_ref()
212 }
213 pub fn reports(&self) -> Vec<Report> {
214 self.reports.clone()
215 }
216}
217
218#[derive(clap::Parser)]
219struct Cli {
220 #[command(subcommand)]
221 command: Option<Command>,
222}
223
224#[derive(clap::Subcommand, Default, Clone)]
225enum Command {
226 #[default]
227 Ui,
228 Check {
229 #[clap(long, short, default_value = "human")]
230 format: OutputFormat,
231 path: PathBuf,
232 },
233}
234
235#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)]
236enum OutputFormat {
237 #[default]
238 Human,
239 Json,
240}
241
242async fn run_impl(hook: Arc<dyn Hook + Send + Sync + 'static>) -> Result<()> {
243 let cli = Cli::parse();
244
245 match cli.command.clone().unwrap_or_default() {
246 Command::Ui => {
247 let endpoints = endpoints();
248
249 populate_js_client(&endpoints);
250
251 let app = Router::new()
252 .nest("/api", Router::new().tapis(endpoints.into_iter()))
253 .route("/", get(index_handler))
254 .route("/index.html", get(index_handler))
255 .route("/{*file}", get(static_handler))
256 .with_state(AppState { hook });
257
258 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
259 axum::serve(listener, app).await.unwrap();
260
261 Ok(())
262 }
263 Command::Check { path, format } => {
264 let src = std::fs::read_to_string(&path)
265 .with_context(|| format!("failed to read '{}'", path.display()))?;
266
267 let reports = match run_hook(&*hook, &src) {
268 Ok(reports) => reports,
269 Err((_, error)) => return Err(error),
270 };
271
272 let diagnostics = reports.into_iter().map(|report| {
273 miette::diagnostic!(
274 labels = vec![miette::LabeledSpan::at(
275 (report.span.start(), report.span.len()),
276 &report.message
277 ),],
278 severity = match report.severity {
279 Severity::Info => miette::Severity::Advice,
280 Severity::Warning => miette::Severity::Warning,
281 Severity::Error => miette::Severity::Error,
282 },
283 "{}",
284 report.message
285 )
286 });
287 match format {
288 OutputFormat::Human => {
289 for diag in diagnostics {
290 let report = miette::Report::new(diag).with_source_code(
291 miette::NamedSource::new(path.display().to_string(), src.to_string()),
292 );
293 println!("{report:?}");
294 }
295 }
296 OutputFormat::Json => {
297 let output = serde_json::to_string(&diagnostics.collect_vec())?;
298 println!("{}", output);
299 }
300 }
301
302 Ok(())
303 }
304 }
305}
306
307fn populate_js_client(endpoints: &tapi::endpoints::Endpoints<AppState>) -> bool {
311 let js_client_path = std::path::PathBuf::from("./crates/slang-ui/static/tapi.js");
312 if js_client_path.exists() {
314 let contents = endpoints.js_client();
316 let prev = std::fs::read_to_string(&js_client_path).unwrap_or_default();
317 if prev != contents {
318 let _ = std::fs::write(&js_client_path, contents);
319 }
320 true
321 } else {
322 false
323 }
324}
325
326async fn index_handler() -> impl IntoResponse {
327 static_handler("/index.html".parse::<Uri>().unwrap()).await
328}
329
330async fn static_handler(uri: Uri) -> impl IntoResponse {
331 StaticFile(uri.path().trim_start_matches('/').to_string())
332}
333
334pub struct StaticFile<T>(pub T);
335
336impl<T> IntoResponse for StaticFile<T>
337where
338 T: Into<String>,
339{
340 fn into_response(self) -> Response {
341 let path = self.0.into();
342
343 match Asset::get(path.as_str()) {
344 Some(content) => {
345 let mime = mime_guess::from_path(path).first_or_octet_stream();
346 ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
347 }
348 None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(),
349 }
350 }
351}
352
353#[derive(Clone)]
354struct AppState {
355 hook: Arc<dyn Hook + Send + Sync>,
356}
357
358impl std::fmt::Debug for AppState {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 f.debug_struct("AppState").field("hook", &"...").finish()
361 }
362}
363
364fn endpoints() -> tapi::endpoints::Endpoints<'static, AppState> {
365 tapi::endpoints::Endpoints::new([
366 &sample_files::endpoint as &dyn tapi::endpoints::Endpoint<AppState>,
367 &heartbeat::endpoint as _,
368 &analyze::endpoint as _,
369 &hover::endpoint as _,
370 ])
371}
372
373fn run_hook(
374 hook: &dyn Hook,
375 src: &str,
376) -> Result<Vec<Report>, (Vec<Report>, color_eyre::eyre::Error)> {
377 let span = tracing::span!(parent: tracing::Span::none(), tracing::Level::INFO, "analyze");
378 let _enter = span.enter();
379
380 let file = slang::parse_file(src);
381 let cx = Context::new(&file);
382 for err in &file.parse_errors {
383 cx.error(err.span(), format!("parse error: {}", err.msg()));
384 }
385 for err in &file.tc_errors {
386 cx.error(err.span(), err.msg());
387 }
388 match hook.analyze(&cx, &file) {
389 Ok(()) => Ok(cx.reports()),
390 Err(err) => Err((cx.reports(), err)),
391 }
392}
393
394#[derive(Debug, Serialize, Deserialize, tapi::Tapi)]
395enum Heartbeat {
396 Alive,
397}
398
399#[tapi::tapi(path = "/heartbeat", method = Get)]
400async fn heartbeat() -> tapi::endpoints::Sse<Heartbeat> {
401 let (tx, rx) = tokio::sync::mpsc::channel(1);
402
403 tokio::spawn(async move {
404 loop {
405 if tx.send(Ok(Heartbeat::Alive)).await.is_err() {
406 break;
407 }
408 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
409 }
410 });
411
412 tapi::endpoints::Sse::new(tokio_stream::wrappers::ReceiverStream::new(rx))
413}
414
415#[derive(Debug, Default, Serialize, tapi::Tapi)]
416struct SampleFiles {
417 files: BTreeMap<String, String>,
418}
419
420#[tapi::tapi(path = "/sample-files", method = Get)]
421async fn sample_files() -> Json<SampleFiles> {
422 let Ok(files) = glob::glob("**/*.slang") else {
423 return Json(SampleFiles::default());
424 };
425 Json(SampleFiles {
426 files: files
427 .filter_map(|f| {
428 let f = f.ok()?;
429 let src = std::fs::read_to_string(&f).ok()?;
430 Some((f.display().to_string(), src))
431 })
432 .collect(),
433 })
434}
435
436#[derive(Debug, Serialize, Deserialize, tapi::Tapi)]
437pub struct AnalyzeParams {
438 file: String,
439}
440
441#[derive(Debug, Serialize, tapi::Tapi)]
442pub struct AnalyzeResult {
443 markers: Vec<monaco::MarkerData>,
444 analysis_errored: bool,
445 message: Option<(Option<String>, Color)>,
446}
447
448#[tapi::tapi(path = "/analyze", method = Post)]
449async fn analyze(state: State<AppState>, params: Json<AnalyzeParams>) -> Json<AnalyzeResult> {
450 let (reports, analysis_errored) = match run_hook(state.hook.as_ref(), ¶ms.file) {
451 Ok(reports) => (reports, false),
452 Err((reports, err)) => {
453 eprintln!("{err:?}");
454 (reports, true)
455 }
456 };
457 Json(AnalyzeResult {
458 markers: reports
459 .iter()
460 .map(|r| monaco::MarkerData {
461 related_information: None,
462 tags: None,
463 severity: match r.severity {
464 Severity::Info => monaco::MarkerSeverity::Info,
465 Severity::Warning => monaco::MarkerSeverity::Warning,
466 Severity::Error => monaco::MarkerSeverity::Error,
467 },
468 message: r.message.clone(),
469 span: monaco::MonacoSpan::from_source_span(¶ms.file, r.span),
470 })
471 .collect(),
472 analysis_errored,
473 message: None,
474 })
475}
476
477#[derive(Debug, Deserialize, tapi::Tapi)]
478pub struct HoverParams {
479 pub file: String,
480 pub pos: monaco::MonacoPosition,
481}
482#[derive(Debug, Serialize, tapi::Tapi)]
483pub struct HoverResult {
484 pub span: monaco::MonacoSpan,
485 pub contents: Vec<String>,
486}
487
488#[tapi::tapi(path = "/hover", method = Post)]
489async fn hover(state: State<AppState>, params: Json<HoverParams>) -> Json<Option<HoverResult>> {
490 let file = slang::parse_file(¶ms.file);
491 let cx = Context::new(&file);
492 let result = match state.hook.hover(
493 &cx,
494 &file,
495 slang::Position {
496 column: params.pos.column,
497 line: params.pos.line_number,
498 },
499 ) {
500 Some(res) => Some(HoverResult {
501 span: monaco::MonacoSpan::from_source_span(¶ms.file, res.span),
502 contents: res.contents,
503 }),
504 None => None,
505 };
506
507 Json(result)
508}