Skip to content

Commit 1f27c95

Browse files
authored
Merge pull request #725 from wado-lang/claude/add-module-scope-attrs-L7XOk
Add #![TODO] and #![generated] module-scope attributes
2 parents 3a4cffb + 8ad3a56 commit 1f27c95

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+554
-86
lines changed

docs/cheatsheet.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,8 @@ Paths in `#include_str` and `#include_bytes` are resolved relative to the source
11211121

11221122
```wado
11231123
#![no_prelude] // disable auto-import of core:prelude
1124+
#![TODO] // all tests must fail; compile errors also accepted
1125+
#![generated] // marks machine-generated code (wado-from-wit, gale)
11241126
11251127
struct Foo {
11261128
#[hidden]

docs/spec.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4393,6 +4393,27 @@ Module-level inner attribute. Prevents the automatic import of `core:prelude`. U
43934393
// This module does not import core:prelude
43944394
```
43954395

4396+
#### `#![TODO]`
4397+
4398+
Module-level inner attribute. Marks the entire module as TODO for `wado test`. The source must parse successfully (otherwise the attribute cannot be recognized), but compilation errors are tolerated. If compilation fails, the module is reported as a passing TODO test. If compilation succeeds, all test blocks are treated as `#[TODO]` tests: they must fail (trap or error). If any test passes, it becomes a test failure, signaling that the `#![TODO]` attribute should be removed.
4399+
4400+
```wado
4401+
#![TODO]
4402+
4403+
test "not yet implemented" {
4404+
panic("TODO");
4405+
}
4406+
```
4407+
4408+
#### `#![generated]`
4409+
4410+
Module-level inner attribute. Indicates that the module contains machine-generated code (e.g. from `wado-from-wit` or `gale`). Stored in the AST but not carried into TIR or later compilation stages. Reserved for future tooling use: language services may prohibit editing generated modules, and coverage tools may exclude them.
4411+
4412+
```wado
4413+
#![generated]
4414+
// This file was generated by wado-from-wit — do not edit manually.
4415+
```
4416+
43964417
#### `#![wasm_module("name")]`
43974418

43984419
Module-level inner attribute. All items in this module are compiled into a **separate Wasm core module** with the given name, rather than into the main GC core module.

package-gale/src/generator.wado

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub fn generate(grammar: &Grammar) -> String {
2020
}
2121

2222
fn gen_header(w: &mut CodeWriter, name: &String) {
23+
w.line("#![generated]");
2324
w.line(`// Generated by Gale from {name}.g4 — do not edit.`);
2425
w.blank();
2526
}

package-gale/src/generator_test.wado

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ test "generate header" {
1919
};
2020
let output = generate(&grammar);
2121
assert output.len() > 0;
22-
let header = "// Generated by Gale from Test.g4";
22+
let header = "#![generated]\n// Generated by Gale from Test.g4";
2323
for let mut i = 0; i < header.len(); i += 1 {
2424
assert output.get_byte(i) == header.get_byte(i);
2525
}

package-gale/tests/golden/calculator.wado

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![generated]
12
// Generated by Gale from calculator.g4 — do not edit.
23

34
/// Span represents a byte range in source text.

package-gale/tests/golden/json.wado

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![generated]
12
// Generated by Gale from JSON.g4 — do not edit.
23

34
/// Span represents a byte range in source text.

package-gale/tests/golden/sexpression.wado

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![generated]
12
// Generated by Gale from sexpression.g4 — do not edit.
23

34
/// Span represents a byte range in source text.

package-gale/tests/golden/sqlite.wado

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![generated]
12
// Generated by Gale from SQLite.g4 — do not edit.
23

34
/// Span represents a byte range in source text.

wado-cli/src/compile.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,50 @@ pub async fn compile_with_opts(
294294
.await
295295
}
296296

297+
/// Try to compile a Wado source file, returning Result instead of exiting on error.
298+
/// Used by the test runner to gracefully handle compilation failures for `#![TODO]` modules.
299+
pub async fn try_compile_with_full_opts(
300+
filename: &str,
301+
opt_level: OptLevel,
302+
log_level: LogLevel,
303+
target_world: Option<String>,
304+
skip_validation: bool,
305+
inline_threshold: Option<usize>,
306+
opt_iterations: Option<u32>,
307+
allocator: Option<String>,
308+
) -> Result<wado_compiler::CompileResult, wado_compiler::CompileFailure> {
309+
let path = Path::new(filename);
310+
311+
let source = match fs::read_to_string(path) {
312+
Ok(s) => s,
313+
Err(e) => {
314+
eprintln!("Error reading '{}': {e}", path.display());
315+
return Err(wado_compiler::CompileFailure {
316+
is_todo_module: false,
317+
});
318+
}
319+
};
320+
321+
let base_path = path
322+
.parent()
323+
.map(std::path::Path::to_path_buf)
324+
.unwrap_or_default();
325+
let host = FilesystemCompilerHost::with_log_level(base_path, log_level);
326+
327+
let options = wado_compiler::CompilerOptions {
328+
opt_level: to_compiler_opt_level(opt_level),
329+
target_world,
330+
skip_validation,
331+
inline_threshold,
332+
opt_iterations,
333+
log_level: Some(log_level),
334+
allocator,
335+
..Default::default()
336+
};
337+
338+
wado_compiler::compile_with_options(&source, &host, Some(filename), options).await
339+
}
340+
297341
/// Compile a Wado source file with full options including target world
298342
pub async fn compile_with_full_opts(
299343
filename: &str,

wado-cli/src/test.rs

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -266,19 +266,31 @@ fn extract_display_name(test_name: &str) -> String {
266266
}
267267
}
268268

269+
/// A `#![TODO]` module that failed to compile.
270+
/// Treated as a passing result since the module is expected to have errors.
271+
struct TodoCompileError {
272+
path: String,
273+
compile_duration: Duration,
274+
}
275+
269276
/// Phase 1: Compile all test files and collect test jobs
270277
async fn collect_test_jobs(
271278
paths: &[String],
272279
filter: Option<&str>,
273-
) -> Result<(Vec<Arc<CompiledTestModule>>, Vec<TestJob>)> {
280+
) -> Result<(
281+
Vec<Arc<CompiledTestModule>>,
282+
Vec<TestJob>,
283+
Vec<TodoCompileError>,
284+
)> {
274285
let mut modules = Vec::new();
275286
let mut jobs = Vec::new();
287+
let mut todo_compile_errors = Vec::new();
276288

277289
for (module_idx, path) in paths.iter().enumerate() {
278290
// Compile with --world test so test functions become component exports
279291
// and non-test code is subject to DCE.
280292
let compile_start = Instant::now();
281-
let wasm = compile::compile_with_full_opts(
293+
let compile_result = compile::try_compile_with_full_opts(
282294
path,
283295
crate::compile::OptLevel::default(),
284296
wado_compiler::LogLevel::default(),
@@ -290,9 +302,26 @@ async fn collect_test_jobs(
290302
)
291303
.await;
292304
let compile_duration = compile_start.elapsed();
305+
306+
let compile_result = match compile_result {
307+
Ok(result) => result,
308+
Err(failure) if failure.is_todo_module => {
309+
// #![TODO] module failed to compile — expected, count as pass
310+
todo_compile_errors.push(TodoCompileError {
311+
path: path.clone(),
312+
compile_duration,
313+
});
314+
continue;
315+
}
316+
Err(_) => {
317+
// Non-TODO module failed to compile — fatal
318+
process::exit(1);
319+
}
320+
};
321+
293322
let load_start = Instant::now();
294323
let engine = Arc::new(runtime::create_test_engine(wasmtime::OptLevel::None)?);
295-
let component = Arc::new(Component::new(&engine, &wasm)?);
324+
let component = Arc::new(Component::new(&engine, &compile_result.wasm)?);
296325
let load_duration = load_start.elapsed();
297326

298327
// Find test functions from exports
@@ -334,7 +363,7 @@ async fn collect_test_jobs(
334363
}));
335364
}
336365

337-
Ok((modules, jobs))
366+
Ok((modules, jobs, todo_compile_errors))
338367
}
339368

340369
/// Run a single test in its own Store
@@ -545,15 +574,16 @@ pub async fn run(opts: TestOptions) {
545574
let overall_start = Instant::now();
546575

547576
// Phase 1: Compile all files and collect test jobs
548-
let (modules, jobs) = match collect_test_jobs(&opts.paths, opts.filter.as_deref()).await {
549-
Ok(result) => result,
550-
Err(e) => {
551-
eprintln!("Error collecting tests: {e}");
552-
process::exit(1);
553-
}
554-
};
577+
let (modules, jobs, todo_compile_errors) =
578+
match collect_test_jobs(&opts.paths, opts.filter.as_deref()).await {
579+
Ok(result) => result,
580+
Err(e) => {
581+
eprintln!("Error collecting tests: {e}");
582+
process::exit(1);
583+
}
584+
};
555585

556-
let total_tests = jobs.len();
586+
let total_tests = jobs.len() + todo_compile_errors.len();
557587
if total_tests == 0 {
558588
println!("No tests found");
559589
return;
@@ -572,6 +602,12 @@ pub async fn run(opts: TestOptions) {
572602
.push(result);
573603
}
574604

605+
// Build lookup maps for display
606+
let todo_error_by_path: indexmap::IndexMap<&str, &TodoCompileError> = todo_compile_errors
607+
.iter()
608+
.map(|e| (e.path.as_str(), e))
609+
.collect();
610+
575611
// Display results in file order (matching input order)
576612
let mut total_passed = 0;
577613
let mut total_failed = 0;
@@ -583,6 +619,15 @@ pub async fn run(opts: TestOptions) {
583619
.collect();
584620

585621
for path in &opts.paths {
622+
// Handle #![TODO] modules that failed to compile
623+
if let Some(todo_err) = todo_error_by_path.get(path.as_str()) {
624+
let compile = format_duration(todo_err.compile_duration);
625+
println!("Running tests in {path}... (compile error, {compile})");
626+
println!(" \x1b[32m✓\x1b[0m #![TODO] module — compile error (expected) ({compile})");
627+
total_passed += 1;
628+
continue;
629+
}
630+
586631
if let Some(file_results) = results_by_file.get(path) {
587632
let timing = module_by_path
588633
.get(path.as_str())

0 commit comments

Comments
 (0)