diff --git a/examples/conditional-imports-wasm.ilo b/examples/conditional-imports-wasm.ilo new file mode 100644 index 00000000..7f412977 --- /dev/null +++ b/examples/conditional-imports-wasm.ilo @@ -0,0 +1,19 @@ +-- Conditional imports example (ILO-399). +-- Syntax: use ? "true-path" : "false-path" +-- +-- Pick different modules at compile time based on the build target. +-- Predicates: wasm (wasm32 target), native (host binary), test (ilo test run). +-- +-- Example (not runnable inline — requires companion files): +-- use ?wasm "platform/wasm-io.ilo" : "platform/native-io.ilo" +-- use ?test "stubs/db-stub.ilo" : "db/real-db.ilo" +-- +-- The resolver evaluates the predicate against the current build target +-- and imports exactly one of the two modules; the other is never read. +-- Both branches follow the same import rules as unconditional `use`: +-- flat import (all public declarations), selective import ([name1 name2]), +-- or named-module alias (use alias: form) are NOT combined with ? — +-- conditional imports always import all declarations from the chosen file. + +-- Standalone demonstration: a function that shows the active platform. +platform>t;"native" diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 03be49f5..1554ac77 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -92,6 +92,29 @@ pub struct Param { pub ty: Type, } +/// Compile-time predicate for conditional `use` — `use ?wasm "a.ilo" : "b.ilo"`. +/// +/// `wasm` — true when building for wasm32 (`--target wasm`). +/// `native` — true when building for a native host (`--target native`, default). +/// `test` — true when running under `ilo test` (`--target test`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum UsePredicate { + Wasm, + Native, + Test, +} + +impl UsePredicate { + pub fn from_str(s: &str) -> Option { + match s { + "wasm" => Some(Self::Wasm), + "native" => Some(Self::Native), + "test" => Some(Self::Test), + _ => None, + } + } +} + /// Top-level declarations #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Decl { @@ -138,6 +161,9 @@ pub enum Decl { /// `use alias:"path/to/file.ilo"` — import all public declarations, prefixed /// with `alias-` (e.g. `math-dbl`, `math-half`). Private (`_`-prefixed) /// symbols are always excluded from named-module imports. + /// `use ?wasm "wasm-mod.ilo" : "native-mod.ilo"` — conditional import: + /// import `path` when the predicate is true for the current build target, + /// otherwise import `alt_path`. Resolved before verification. /// Resolved before verification; replaced by the imported declarations in /// the merged program. Stripped by the verifier/codegen as a safety net. Use { @@ -147,6 +173,12 @@ pub enum Decl { /// Named module alias: `use alias:"path"` sets this to `Some("alias")`. /// When set, imported public symbols are renamed `alias-`. alias: Option, + /// Conditional form: `use ? "true-path" : "false-path"`. + /// When `Some`, `path` is the true-branch and `alt_path` is the + /// false-branch. `only` and `alias` are disallowed in this form. + predicate: Option, + /// The false-branch path for conditional imports. `None` for unconditional. + alt_path: Option, #[serde(skip)] span: Span, }, diff --git a/src/codegen/explain.rs b/src/codegen/explain.rs index 3d2ad2be..8eb9d6ad 100644 --- a/src/codegen/explain.rs +++ b/src/codegen/explain.rs @@ -551,6 +551,8 @@ mod tests { path: "x.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: Span::UNKNOWN, }); prog.declarations.push(Decl::Error { diff --git a/src/codegen/fmt.rs b/src/codegen/fmt.rs index 2357a36f..31cd6b1f 100644 --- a/src/codegen/fmt.rs +++ b/src/codegen/fmt.rs @@ -1298,6 +1298,8 @@ mod tests { path: "x.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: Span::UNKNOWN, }; let s = format_decl(&use_decl, FmtMode::Dense); diff --git a/src/codegen/python.rs b/src/codegen/python.rs index 29d0148d..9d610ee1 100644 --- a/src/codegen/python.rs +++ b/src/codegen/python.rs @@ -2278,6 +2278,8 @@ mod tests { path: "x.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: Span::UNKNOWN, }); let py = emit(&prog); diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index db068952..ee552804 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -14370,6 +14370,8 @@ mod tests { path: "x.ilo".to_string(), only: None, alias: None, + predicate: None, + alt_path: None, span: Span { start: 0, end: 0 }, }, ); diff --git a/src/main.rs b/src/main.rs index 288d1297..a4b7b7c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1488,6 +1488,7 @@ fn compile_cmd(args: &[String]) -> i32 { base_dir.as_deref(), &mut visited, &mut import_diagnostics, + BuildTarget::default(), ); if !import_diagnostics.is_empty() { for d in &import_diagnostics { @@ -2251,6 +2252,34 @@ fn decl_name(decl: &ast::Decl) -> Option<&str> { } } +/// Build target used to evaluate conditional `use ?` predicates. +/// +/// Passed to `resolve_imports` so that `use ?wasm "a" : "b"` picks the +/// right branch at compile time without touching the runtime. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum BuildTarget { + /// Native host binary (default). + #[default] + Native, + /// WebAssembly (wasm32) target. + Wasm, + /// Test run (`ilo test`). + Test, +} + +impl BuildTarget { + /// Evaluate a `UsePredicate` against this target: returns `true` when + /// the predicate matches (i.e. the "true" branch should be imported). + pub fn eval(self, pred: ast::UsePredicate) -> bool { + match (self, pred) { + (BuildTarget::Wasm, ast::UsePredicate::Wasm) => true, + (BuildTarget::Native, ast::UsePredicate::Native) => true, + (BuildTarget::Test, ast::UsePredicate::Test) => true, + _ => false, + } + } +} + /// Resolve all `Decl::Use` nodes in `decls` recursively, returning a flat /// merged list with imported declarations prepended and `Use` nodes stripped. /// @@ -2258,6 +2287,7 @@ fn decl_name(decl: &ast::Decl) -> Option<&str> { /// `None` means inline code — `use` is not supported without a file context. /// - `visited`: canonical paths already in the import chain; circular imports are errors. /// - `diagnostics`: errors are pushed here (file-not-found, circular, parse failures). +/// - `build_target`: evaluates `use ?` predicates at compile time. /// /// Privacy rule: declarations whose name starts with `_` are module-private and /// are never exported. They are stripped during import regardless of `only` or `alias`. @@ -2266,6 +2296,7 @@ fn resolve_imports( base_dir: Option<&std::path::Path>, visited: &mut std::collections::HashSet, diagnostics: &mut Vec, + build_target: BuildTarget, ) -> Vec { let mut result: Vec = Vec::new(); @@ -2274,9 +2305,24 @@ fn resolve_imports( path, only, alias, + predicate, + alt_path, span, } = decl { + // For conditional imports, select the right branch now. + let path = if let Some(pred) = predicate { + if build_target.eval(pred) { + path + } else { + // alt_path is guaranteed to be Some when predicate is Some + // (enforced by the parser). + alt_path.unwrap_or(path) + } + } else { + path + }; + let Some(dir) = base_dir else { diagnostics.push( Diagnostic::error( @@ -2357,6 +2403,7 @@ fn resolve_imports( imported_dir, visited, diagnostics, + build_target, ); visited.remove(&canonical); @@ -3456,6 +3503,7 @@ fn check_cmd(source_arg: &str, mode: OutputMode, _explicit_json: bool, strict: b base_dir.as_deref(), &mut visited, &mut import_diagnostics, + BuildTarget::default(), ); for d in import_diagnostics { report_diagnostic(&d, mode); @@ -3684,6 +3732,7 @@ fn dispatch_run( base_dir.as_deref(), &mut visited, &mut import_diagnostics, + BuildTarget::default(), ); for d in import_diagnostics { report_diagnostic(&d, mode); @@ -6390,6 +6439,8 @@ mod tests { path: "lib.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; assert_eq!(decl_name(&d), None); @@ -6428,6 +6479,8 @@ mod tests { path: "ilo_test_resolve_only_F2G7.ilo".into(), only: Some(vec!["dbl".into()]), alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; let mut diags = Vec::new(); @@ -6437,6 +6490,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); let names: Vec<&str> = result.iter().filter_map(|d| decl_name(d)).collect(); @@ -6462,6 +6516,8 @@ mod tests { path: "ilo_test_resolve_missing_H4K9.ilo".into(), only: Some(vec!["dbl".into(), "nonexistent".into()]), alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; let mut diags = Vec::new(); @@ -6471,6 +6527,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!( @@ -6715,11 +6772,19 @@ mod tests { path: "something.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 20 }, }; let mut visited = std::collections::HashSet::new(); let mut diags = Vec::new(); - let result = resolve_imports(vec![use_decl], None, &mut visited, &mut diags); + let result = resolve_imports( + vec![use_decl], + None, + &mut visited, + &mut diags, + BuildTarget::default(), + ); assert!(result.is_empty()); assert!(diags.iter().any(|d| d.code == Some("ILO-P017"))); assert!(diags[0].message.contains("inline code")); @@ -6731,6 +6796,8 @@ mod tests { path: "nonexistent_xyz_99999.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 30 }, }; let mut visited = std::collections::HashSet::new(); @@ -6740,6 +6807,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!(result.is_empty()); assert!( @@ -6760,7 +6828,13 @@ mod tests { }; let mut visited = std::collections::HashSet::new(); let mut diags = Vec::new(); - let result = resolve_imports(vec![func_decl], None, &mut visited, &mut diags); + let result = resolve_imports( + vec![func_decl], + None, + &mut visited, + &mut diags, + BuildTarget::default(), + ); assert_eq!(result.len(), 1); assert!(diags.is_empty()); } @@ -6846,6 +6920,8 @@ mod tests { path: "ilo_unit_bad_parse_imports.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }]; let mut visited = std::collections::HashSet::new(); @@ -6855,6 +6931,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!( @@ -6882,6 +6959,8 @@ mod tests { path: "ilo_unit_trans_a_Q3R8.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }]; let mut visited = std::collections::HashSet::new(); @@ -6891,6 +6970,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}"); @@ -6917,6 +6997,8 @@ mod tests { path: "ilo_test_alias_rename_X9Y2.ilo".into(), only: None, alias: Some("m".into()), + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; let mut diags = Vec::new(); @@ -6926,6 +7008,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!(diags.is_empty(), "no errors expected: {diags:?}"); @@ -6953,6 +7036,8 @@ mod tests { path: "ilo_test_alias_priv_W7Z4.ilo".into(), only: None, alias: Some("m".into()), + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; let mut diags = Vec::new(); @@ -6962,6 +7047,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!(diags.is_empty(), "no errors expected: {diags:?}"); @@ -6992,6 +7078,8 @@ mod tests { path: "ilo_test_sel_priv_V3K8.ilo".into(), only: Some(vec!["_priv".into()]), alias: None, + predicate: None, + alt_path: None, span: ast::Span { start: 0, end: 0 }, }; let mut diags = Vec::new(); @@ -7001,6 +7089,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diags, + BuildTarget::default(), ); assert!(result.is_empty(), "private name should produce no result"); @@ -7016,6 +7105,141 @@ mod tests { std::fs::remove_file(lib_path).ok(); } + // ── resolve_imports: conditional use ? (ILO-399) ──────────────────── + + #[test] + fn resolve_imports_conditional_wasm_true_branch() { + // `use ?wasm "wasm.ilo" : "native.ilo"` with BuildTarget::Wasm → loads wasm.ilo + use std::io::Write; + let wasm_path = "/tmp/ilo_cond_wasm_ILO399.ilo"; + let native_path = "/tmp/ilo_cond_native_ILO399.ilo"; + std::fs::write(wasm_path, "wasm-fn>n;42").unwrap(); + std::fs::write(native_path, "native-fn>n;99").unwrap(); + + let use_decl = ast::Decl::Use { + path: "ilo_cond_wasm_ILO399.ilo".into(), + only: None, + alias: None, + predicate: Some(ast::UsePredicate::Wasm), + alt_path: Some("ilo_cond_native_ILO399.ilo".into()), + span: ast::Span::UNKNOWN, + }; + let mut diags = Vec::new(); + let mut visited = std::collections::HashSet::new(); + let result = resolve_imports( + vec![use_decl], + Some(std::path::Path::new("/tmp")), + &mut visited, + &mut diags, + BuildTarget::Wasm, + ); + + assert!(diags.is_empty(), "no errors expected: {diags:?}"); + let names: Vec<_> = result.iter().filter_map(|d| decl_name(d)).collect(); + assert!( + names.contains(&"wasm-fn"), + "expected wasm-fn, got: {names:?}" + ); + assert!( + !names.contains(&"native-fn"), + "native-fn should not be imported" + ); + + std::fs::remove_file(wasm_path).ok(); + std::fs::remove_file(native_path).ok(); + } + + #[test] + fn resolve_imports_conditional_wasm_false_branch() { + // `use ?wasm "wasm.ilo" : "native.ilo"` with BuildTarget::Native → loads native.ilo + let wasm_path = "/tmp/ilo_cond2_wasm_ILO399.ilo"; + let native_path = "/tmp/ilo_cond2_native_ILO399.ilo"; + std::fs::write(wasm_path, "wasm-fn>n;42").unwrap(); + std::fs::write(native_path, "native-fn>n;99").unwrap(); + + let use_decl = ast::Decl::Use { + path: "ilo_cond2_wasm_ILO399.ilo".into(), + only: None, + alias: None, + predicate: Some(ast::UsePredicate::Wasm), + alt_path: Some("ilo_cond2_native_ILO399.ilo".into()), + span: ast::Span::UNKNOWN, + }; + let mut diags = Vec::new(); + let mut visited = std::collections::HashSet::new(); + let result = resolve_imports( + vec![use_decl], + Some(std::path::Path::new("/tmp")), + &mut visited, + &mut diags, + BuildTarget::Native, // default — not wasm + ); + + assert!(diags.is_empty(), "no errors expected: {diags:?}"); + let names: Vec<_> = result.iter().filter_map(|d| decl_name(d)).collect(); + assert!( + names.contains(&"native-fn"), + "expected native-fn, got: {names:?}" + ); + assert!( + !names.contains(&"wasm-fn"), + "wasm-fn should not be imported" + ); + + std::fs::remove_file(wasm_path).ok(); + std::fs::remove_file(native_path).ok(); + } + + #[test] + fn resolve_imports_conditional_test_predicate() { + // `use ?test "stub.ilo" : "real.ilo"` with BuildTarget::Test → loads stub + let stub_path = "/tmp/ilo_cond_stub_ILO399.ilo"; + let real_path = "/tmp/ilo_cond_real_ILO399.ilo"; + std::fs::write(stub_path, "stub-fn>n;0").unwrap(); + std::fs::write(real_path, "real-fn>n;1").unwrap(); + + let use_decl = ast::Decl::Use { + path: "ilo_cond_stub_ILO399.ilo".into(), + only: None, + alias: None, + predicate: Some(ast::UsePredicate::Test), + alt_path: Some("ilo_cond_real_ILO399.ilo".into()), + span: ast::Span::UNKNOWN, + }; + let mut diags = Vec::new(); + let mut visited = std::collections::HashSet::new(); + let result = resolve_imports( + vec![use_decl], + Some(std::path::Path::new("/tmp")), + &mut visited, + &mut diags, + BuildTarget::Test, + ); + + assert!(diags.is_empty(), "no errors: {diags:?}"); + let names: Vec<_> = result.iter().filter_map(|d| decl_name(d)).collect(); + assert!( + names.contains(&"stub-fn"), + "expected stub-fn, got: {names:?}" + ); + + std::fs::remove_file(stub_path).ok(); + std::fs::remove_file(real_path).ok(); + } + + #[test] + fn build_target_eval_all_predicates() { + assert!(BuildTarget::Wasm.eval(ast::UsePredicate::Wasm)); + assert!(!BuildTarget::Wasm.eval(ast::UsePredicate::Native)); + assert!(!BuildTarget::Wasm.eval(ast::UsePredicate::Test)); + assert!(BuildTarget::Native.eval(ast::UsePredicate::Native)); + assert!(!BuildTarget::Native.eval(ast::UsePredicate::Wasm)); + assert!(!BuildTarget::Native.eval(ast::UsePredicate::Test)); + assert!(BuildTarget::Test.eval(ast::UsePredicate::Test)); + assert!(!BuildTarget::Test.eval(ast::UsePredicate::Wasm)); + assert!(!BuildTarget::Test.eval(ast::UsePredicate::Native)); + } + // ── report_diagnostic: all three output modes ───────────────────────────── #[test] @@ -8077,6 +8301,8 @@ mod tests { path: path.to_string(), only: None, alias: None, + predicate: None, + alt_path: None, span: ast::Span::UNKNOWN, } } @@ -8087,7 +8313,13 @@ mod tests { let decls = vec![make_use_decl("math.ilo")]; let mut visited = std::collections::HashSet::new(); let mut diagnostics = Vec::new(); - let result = resolve_imports(decls, None, &mut visited, &mut diagnostics); + let result = resolve_imports( + decls, + None, + &mut visited, + &mut diagnostics, + BuildTarget::default(), + ); assert!(result.is_empty(), "should return no decls"); assert!(!diagnostics.is_empty(), "should emit error"); assert!(diagnostics[0].message.contains("file path context")); @@ -8100,7 +8332,13 @@ mod tests { let mut visited = std::collections::HashSet::new(); let mut diagnostics = Vec::new(); let dir = std::path::Path::new("/tmp"); - let result = resolve_imports(decls, Some(dir), &mut visited, &mut diagnostics); + let result = resolve_imports( + decls, + Some(dir), + &mut visited, + &mut diagnostics, + BuildTarget::default(), + ); assert!(result.is_empty()); assert!(!diagnostics.is_empty()); assert!(diagnostics[0].message.contains("nonexistent_file_xyz.ilo")); @@ -8118,7 +8356,13 @@ mod tests { visited.insert(canonical); let mut diagnostics = Vec::new(); let dir = std::path::Path::new("/tmp"); - let result = resolve_imports(decls, Some(dir), &mut visited, &mut diagnostics); + let result = resolve_imports( + decls, + Some(dir), + &mut visited, + &mut diagnostics, + BuildTarget::default(), + ); assert!(result.is_empty()); assert!(!diagnostics.is_empty()); assert!(diagnostics[0].message.contains("circular")); @@ -8134,7 +8378,13 @@ mod tests { let mut visited = std::collections::HashSet::new(); let mut diagnostics = Vec::new(); let dir = std::path::Path::new("/tmp"); - let _result = resolve_imports(decls, Some(dir), &mut visited, &mut diagnostics); + let _result = resolve_imports( + decls, + Some(dir), + &mut visited, + &mut diagnostics, + BuildTarget::default(), + ); assert!(!diagnostics.is_empty(), "should emit lex error diagnostic"); std::fs::remove_file(path).ok(); } @@ -8156,7 +8406,13 @@ mod tests { let mut visited = std::collections::HashSet::new(); let mut diagnostics = Vec::new(); let dir = std::path::Path::new("/tmp"); - resolve_imports(decls, Some(dir), &mut visited, &mut diagnostics); + resolve_imports( + decls, + Some(dir), + &mut visited, + &mut diagnostics, + BuildTarget::default(), + ); assert!(!diagnostics.is_empty()); } @@ -8362,6 +8618,7 @@ mod tests { Some(std::path::Path::new("/tmp")), &mut visited, &mut diagnostics, + BuildTarget::default(), ); assert!(result.is_empty()); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b4c82e7e..7db6e1f8 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -848,6 +848,122 @@ statement boundary; bind the chain to a local first. For example, split \ let start = self.peek_span(); self.expect(&Token::Use)?; + // Detect conditional import form: `use ? "true-path" : "false-path"`. + // `?` must appear immediately after `use` with no other tokens in between. + if self.peek() == Some(&Token::Question) { + self.advance(); // consume `?` + // Expect a predicate name identifier. + let pred_name = match self.peek().cloned() { + Some(Token::Ident(name)) => { + self.advance(); + name + } + Some(tok) => { + return Err(self.error( + "ILO-P016", + format!( + "expected a predicate name (wasm/native/test) after `use ?`, got {}", + tok.user_facing_name() + ), + )); + } + None => { + return Err(self.error( + "ILO-P016", + "expected a predicate name (wasm/native/test) after `use ?`, got EOF" + .into(), + )); + } + }; + let predicate = match UsePredicate::from_str(&pred_name) { + Some(p) => p, + None => { + return Err(self.error( + "ILO-P016", + format!( + "unknown `use ?` predicate `{pred_name}` — valid predicates: wasm, native, test" + ), + )); + } + }; + // Expect true-branch path string. + let true_path = match self.peek().cloned() { + Some(Token::Text(p)) => { + self.advance(); + p + } + Some(tok) => { + return Err(self.error( + "ILO-P016", + format!( + "expected a string path after `use ?{pred_name}`, got {}", + tok.user_facing_name() + ), + )); + } + None => { + return Err(self.error( + "ILO-P016", + format!("expected a string path after `use ?{pred_name}`, got EOF"), + )); + } + }; + // Expect `:` separator. + match self.peek().cloned() { + Some(Token::Colon) => { + self.advance(); + } + Some(tok) => { + return Err(self.error( + "ILO-P016", + format!( + "expected `:` after true-branch path in `use ?{pred_name}`, got {}", + tok.user_facing_name() + ), + )); + } + None => { + return Err(self.error( + "ILO-P016", + format!( + "expected `:` after true-branch path in `use ?{pred_name}`, got EOF" + ), + )); + } + }; + // Expect false-branch path string. + let false_path = match self.peek().cloned() { + Some(Token::Text(p)) => { + self.advance(); + p + } + Some(tok) => { + return Err(self.error( + "ILO-P016", + format!( + "expected a string path after `:` in `use ?{pred_name}`, got {}", + tok.user_facing_name() + ), + )); + } + None => { + return Err(self.error( + "ILO-P016", + format!("expected a string path after `:` in `use ?{pred_name}`, got EOF"), + )); + } + }; + let end = self.peek_span(); + return Ok(Decl::Use { + path: true_path, + only: None, + alias: None, + predicate: Some(predicate), + alt_path: Some(false_path), + span: start.merge(end), + }); + } + // Detect named-module form: `use alias:"path"` — ident immediately // followed by `:` then a string literal. // Distinguished from the plain form `use "path"` by the leading ident. @@ -956,6 +1072,8 @@ statement boundary; bind the chain to a local first. For example, split \ path, only, alias, + predicate: None, + alt_path: None, span: start.merge(end), }) } @@ -9142,6 +9260,84 @@ mod tests { ); } + // --- conditional use (ILO-399) --- + + #[test] + fn parse_use_conditional_wasm() { + let prog = parse_str(r#"use ?wasm "wasm-mod.ilo" : "native-mod.ilo""#); + let Decl::Use { + path, + alt_path, + predicate, + only, + alias, + .. + } = &prog.declarations[0] + else { + panic!("expected Use, got {:?}", prog.declarations) + }; + assert_eq!(path, "wasm-mod.ilo"); + assert_eq!(alt_path.as_deref(), Some("native-mod.ilo")); + assert_eq!(*predicate, Some(UsePredicate::Wasm)); + assert!(only.is_none()); + assert!(alias.is_none()); + } + + #[test] + fn parse_use_conditional_native() { + let prog = parse_str(r#"use ?native "native.ilo" : "fallback.ilo""#); + let Decl::Use { + predicate, + path, + alt_path, + .. + } = &prog.declarations[0] + else { + panic!("expected Use") + }; + assert_eq!(*predicate, Some(UsePredicate::Native)); + assert_eq!(path, "native.ilo"); + assert_eq!(alt_path.as_deref(), Some("fallback.ilo")); + } + + #[test] + fn parse_use_conditional_test() { + let prog = parse_str(r#"use ?test "test-stubs.ilo" : "real.ilo""#); + let Decl::Use { predicate, .. } = &prog.declarations[0] else { + panic!("expected Use") + }; + assert_eq!(*predicate, Some(UsePredicate::Test)); + } + + #[test] + fn parse_use_conditional_unknown_predicate_error() { + let (_, errors) = parse_str_errors(r#"use ?gpu "a.ilo" : "b.ilo""#); + assert!( + errors + .iter() + .any(|e| e.code == "ILO-P016" && e.message.contains("unknown")), + "expected ILO-P016 unknown predicate, got: {errors:?}" + ); + } + + #[test] + fn parse_use_conditional_missing_false_branch_error() { + let (_, errors) = parse_str_errors(r#"use ?wasm "a.ilo""#); + assert!( + !errors.is_empty(), + "expected error for missing false branch" + ); + } + + #[test] + fn parse_use_conditional_missing_colon_error() { + let (_, errors) = parse_str_errors(r#"use ?wasm "a.ilo" "b.ilo""#); + assert!( + errors.iter().any(|e| e.code == "ILO-P016"), + "expected ILO-P016 for missing colon: {errors:?}" + ); + } + #[test] fn parse_private_fn_decl() { let prog = parse_str("_helper n:n>n;+n 1"); diff --git a/src/verify.rs b/src/verify.rs index 1bb3913e..f9ad9467 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -9579,6 +9579,8 @@ mod tests { path: "x.ilo".into(), only: None, alias: None, + predicate: None, + alt_path: None, span: Span::UNKNOWN, }); let result = verify(&program);