Skip to content

Commit d74e427

Browse files
Claude/add academic proofs g3z zu (#11)
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6d01607 commit d74e427

21 files changed

Lines changed: 3469 additions & 104 deletions

File tree

Cargo.lock

Lines changed: 964 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"crates/anv-ir",
1212
"crates/anv-viz",
1313
"crates/anv-cli",
14+
"crates/anv-lsp",
1415
]
1516

1617
[workspace.package]

crates/anv-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ clap = { workspace = true }
2727
miette = { workspace = true }
2828
thiserror = { workspace = true }
2929
serde_json = { workspace = true }
30+
notify = "7.0"
31+
notify-debouncer-mini = "0.5"

crates/anv-cli/src/main.rs

Lines changed: 223 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ use anv_types::check;
1717
use anv_viz::{RinkRenderer, SvgOptions, TimelineRenderer};
1818
use clap::{Parser, Subcommand};
1919
use miette::{Diagnostic, NamedSource, Report, SourceSpan};
20+
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
2021
use std::fs;
2122
use std::path::PathBuf;
2223
use std::process::ExitCode;
24+
use std::sync::mpsc::channel;
25+
use std::time::Duration;
2326

2427
/// Anvomidav - A domain-specific language for figure skating choreography.
2528
#[derive(Parser)]
@@ -50,7 +53,8 @@ enum Commands {
5053
EXAMPLES:
5154
anv check program.anv
5255
anv check *.anv --verbose
53-
anv check short.anv free.anv")]
56+
anv check short.anv free.anv
57+
anv check *.anv --watch (watch for changes)")]
5458
Check {
5559
/// Source files to check
5660
#[arg(required = true)]
@@ -59,6 +63,10 @@ EXAMPLES:
5963
/// Show detailed output
6064
#[arg(short, long)]
6165
verbose: bool,
66+
67+
/// Watch for file changes and re-check automatically
68+
#[arg(short, long)]
69+
watch: bool,
6270
},
6371

6472
/// Parse a source file and print the AST
@@ -92,14 +100,19 @@ EXAMPLES:
92100
93101
EXAMPLES:
94102
anv fmt program.anv
95-
anv fmt *.anv --check (verify formatting without changes)")]
103+
anv fmt *.anv --check (verify formatting without changes)
104+
anv fmt *.anv --watch (watch for changes)")]
96105
Fmt {
97106
/// Source files to format
98107
files: Vec<PathBuf>,
99108

100109
/// Check if files are formatted without modifying them
101110
#[arg(long)]
102111
check: bool,
112+
113+
/// Watch for file changes and re-format automatically
114+
#[arg(short, long)]
115+
watch: bool,
103116
},
104117

105118
/// Create a new Anvomidav project
@@ -262,66 +275,214 @@ fn main() -> ExitCode {
262275
}
263276
}
264277

265-
fn run(cli: Cli) -> miette::Result<()> {
266-
match cli.command {
267-
Commands::Check { files, verbose } => {
268-
let mut has_errors = false;
278+
/// Check files for errors, returning true if all passed.
279+
fn check_files(files: &[PathBuf], verbose: bool) -> bool {
280+
let mut has_errors = false;
281+
282+
for (file_idx, path) in files.iter().enumerate() {
283+
if verbose {
284+
eprintln!("Checking {}...", path.display());
285+
}
286+
287+
let source = match fs::read_to_string(path) {
288+
Ok(s) => s,
289+
Err(e) => {
290+
eprintln!("Failed to read {}: {}", path.display(), e);
291+
has_errors = true;
292+
continue;
293+
}
294+
};
295+
296+
let file_id = FileId(file_idx as u32);
269297

270-
for (file_idx, path) in files.iter().enumerate() {
271-
if verbose {
272-
eprintln!("Checking {}...", path.display());
298+
// Parse
299+
let program = match parse(&source, file_id) {
300+
Ok(p) => p,
301+
Err(errors) => {
302+
has_errors = true;
303+
for err in errors {
304+
let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into();
305+
let report = Report::new(CliError::ParseError {
306+
src: NamedSource::new(path.display().to_string(), source.clone()),
307+
span,
308+
message: err.message.clone(),
309+
label: err.label.clone().unwrap_or_else(|| "here".into()),
310+
help: err.help.clone(),
311+
});
312+
eprintln!("{:?}", report);
313+
}
314+
continue;
315+
}
316+
};
317+
318+
// Type check
319+
if let Err(diagnostics) = check(&program, file_id) {
320+
has_errors = true;
321+
for diag in diagnostics.iter() {
322+
let code_str = diag.code.as_ref().map(|c| c.to_string()).unwrap_or_default();
323+
eprintln!(
324+
"{}:{}: {} [{}]",
325+
path.display(),
326+
diag.labels.first().map(|l| l.span.start).unwrap_or(0),
327+
diag.message,
328+
code_str
329+
);
330+
for note in &diag.notes {
331+
eprintln!(" note: {:?}", note);
273332
}
333+
}
334+
}
335+
}
274336

275-
let source = fs::read_to_string(path).map_err(|e| CliError::ReadError {
276-
path: path.display().to_string(),
277-
source: e,
278-
})?;
279-
280-
let file_id = FileId(file_idx as u32);
281-
282-
// Parse
283-
let program = match parse(&source, file_id) {
284-
Ok(p) => p,
285-
Err(errors) => {
286-
has_errors = true;
287-
for err in errors {
288-
let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into();
289-
let report = Report::new(CliError::ParseError {
290-
src: NamedSource::new(path.display().to_string(), source.clone()),
291-
span,
292-
message: err.message.clone(),
293-
label: err.label.clone().unwrap_or_else(|| "here".into()),
294-
help: err.help.clone(),
295-
});
296-
eprintln!("{:?}", report);
297-
}
298-
continue;
299-
}
300-
};
337+
if !has_errors && verbose {
338+
eprintln!("All checks passed!");
339+
}
301340

302-
// Type check
303-
if let Err(diagnostics) = check(&program, file_id) {
304-
has_errors = true;
305-
for diag in diagnostics.iter() {
306-
let code_str = diag.code.as_ref().map(|c| c.to_string()).unwrap_or_default();
307-
eprintln!(
308-
"{}:{}: {} [{}]",
309-
path.display(),
310-
diag.labels.first().map(|l| l.span.start).unwrap_or(0),
311-
diag.message,
312-
code_str
313-
);
314-
for note in &diag.notes {
315-
eprintln!(" note: {:?}", note);
316-
}
317-
}
341+
!has_errors
342+
}
343+
344+
/// Format files, returning true if all formatted successfully.
345+
fn format_files(files: &[PathBuf], check_only: bool) -> bool {
346+
if files.is_empty() {
347+
eprintln!("No files specified");
348+
return true;
349+
}
350+
351+
let mut success = true;
352+
353+
for path in files {
354+
let source = match fs::read_to_string(path) {
355+
Ok(s) => s,
356+
Err(e) => {
357+
eprintln!("Failed to read {}: {}", path.display(), e);
358+
success = false;
359+
continue;
360+
}
361+
};
362+
363+
// Format the source
364+
let formatted = match anv_syntax::format::format(&source) {
365+
Ok(f) => f,
366+
Err(errors) => {
367+
for err in errors {
368+
let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into();
369+
let report = Report::new(CliError::ParseError {
370+
src: NamedSource::new(path.display().to_string(), source.clone()),
371+
span,
372+
message: err.message.clone(),
373+
label: err.label.clone().unwrap_or_else(|| "here".into()),
374+
help: err.help.clone(),
375+
});
376+
eprintln!("{:?}", report);
318377
}
378+
success = false;
379+
continue;
319380
}
381+
};
320382

321-
if has_errors {
322-
Err(miette::miette!("check failed with errors"))?;
323-
} else if verbose {
324-
eprintln!("All checks passed!");
383+
let changed = source != formatted;
384+
385+
if check_only {
386+
if changed {
387+
eprintln!("{}: would be reformatted", path.display());
388+
success = false;
389+
} else {
390+
eprintln!("{}: ok", path.display());
391+
}
392+
} else if changed {
393+
if let Err(e) = fs::write(path, &formatted) {
394+
eprintln!("Failed to write {}: {}", path.display(), e);
395+
success = false;
396+
} else {
397+
eprintln!("{}: formatted", path.display());
398+
}
399+
} else {
400+
eprintln!("{}: unchanged", path.display());
401+
}
402+
}
403+
404+
success
405+
}
406+
407+
/// Watch files for changes and run a callback when they change.
408+
fn watch_files<F>(files: &[PathBuf], mut on_change: F) -> miette::Result<()>
409+
where
410+
F: FnMut() -> bool,
411+
{
412+
eprintln!("Watching for changes... (press Ctrl+C to stop)");
413+
414+
// Run once initially
415+
on_change();
416+
417+
// Set up file watcher
418+
let (tx, rx) = channel();
419+
let mut debouncer = new_debouncer(Duration::from_millis(300), tx)
420+
.map_err(|e| miette::miette!("Failed to create watcher: {}", e))?;
421+
422+
// Watch parent directories of all files
423+
let mut watched_dirs = std::collections::HashSet::new();
424+
for path in files {
425+
if let Some(parent) = path.parent() {
426+
let parent = if parent.as_os_str().is_empty() {
427+
PathBuf::from(".")
428+
} else {
429+
parent.to_path_buf()
430+
};
431+
if watched_dirs.insert(parent.clone()) {
432+
debouncer
433+
.watcher()
434+
.watch(&parent, RecursiveMode::NonRecursive)
435+
.map_err(|e| miette::miette!("Failed to watch {}: {}", parent.display(), e))?;
436+
}
437+
}
438+
}
439+
440+
// Process events
441+
loop {
442+
match rx.recv() {
443+
Ok(Ok(events)) => {
444+
// Check if any of our files changed
445+
let our_files: std::collections::HashSet<_> = files
446+
.iter()
447+
.filter_map(|p| p.canonicalize().ok())
448+
.collect();
449+
450+
let changed = events.iter().any(|event| {
451+
event
452+
.path
453+
.canonicalize()
454+
.map(|p| our_files.contains(&p))
455+
.unwrap_or(false)
456+
});
457+
458+
if changed {
459+
eprintln!("\n--- File changed, re-running... ---\n");
460+
on_change();
461+
}
462+
}
463+
Ok(Err(error)) => {
464+
eprintln!("Watch error: {}", error);
465+
}
466+
Err(e) => {
467+
eprintln!("Watch channel error: {}", e);
468+
break;
469+
}
470+
}
471+
}
472+
473+
Ok(())
474+
}
475+
476+
fn run(cli: Cli) -> miette::Result<()> {
477+
match cli.command {
478+
Commands::Check { files, verbose, watch } => {
479+
if watch {
480+
watch_files(&files, || check_files(&files, verbose))?;
481+
} else {
482+
let success = check_files(&files, verbose);
483+
if !success {
484+
return Err(miette::miette!("check failed with errors"));
485+
}
325486
}
326487

327488
Ok(())
@@ -384,36 +545,13 @@ fn run(cli: Cli) -> miette::Result<()> {
384545
Ok(())
385546
}
386547

387-
Commands::Fmt { files, check } => {
388-
if files.is_empty() {
389-
eprintln!("No files specified");
390-
return Ok(());
391-
}
392-
393-
for path in &files {
394-
let source = fs::read_to_string(path).map_err(|e| CliError::ReadError {
395-
path: path.display().to_string(),
396-
source: e,
397-
})?;
398-
399-
// TODO: Implement formatter
400-
// For now, just verify the file parses
401-
let _program = parse(&source, FileId(0)).map_err(|errors| {
402-
let err = &errors[0];
403-
let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into();
404-
CliError::ParseError {
405-
src: NamedSource::new(path.display().to_string(), source.clone()),
406-
span,
407-
message: err.message.clone(),
408-
label: err.label.clone().unwrap_or_else(|| "here".into()),
409-
help: err.help.clone(),
410-
}
411-
})?;
412-
413-
if check {
414-
eprintln!("{}: would reformat", path.display());
415-
} else {
416-
eprintln!("{}: formatted (no-op, formatter not implemented)", path.display());
548+
Commands::Fmt { files, check: check_only, watch } => {
549+
if watch {
550+
watch_files(&files, || format_files(&files, check_only))?;
551+
} else {
552+
let success = format_files(&files, check_only);
553+
if !success && check_only {
554+
return Err(miette::miette!("Some files would be reformatted"));
417555
}
418556
}
419557

0 commit comments

Comments
 (0)