From 27212d82753e8f1fc9fa9cd5f22421ba717e9b2c Mon Sep 17 00:00:00 2001 From: Jeff Roberts Date: Wed, 23 Sep 2026 12:16:49 -0500 Subject: [PATCH] SET PROCEDURE: routines in procedure files are found SET PROCEDURE TO was accepted and ignored, so a bare call into a procedure file failed - which stops a CodeMine application at its first line. Now the files are loaded through the host's LoadProgram, as DO loads a program, without running their main body, and a bare call or DO looks in the current program, then the procedure files in the order listed, then the programs that have run. Measured in Visual FoxPro 9 and written into the set_procedure golden: - ADDITIVE appends; a file already listed stays where it is - without ADDITIVE the list is emptied first and files go on as they load, so a missing file (error 1, "File 'x.prg' does not exist.") keeps the ones before it and drops what was listed before - RELEASE PROCEDURE takes a file off, and its routines stop being found, while a program that ran with DO keeps its routines findable after it returns - SET("PROCEDURE") answers each file's full .FXP path, quoted, comma-separated - a call to a name that is no routine anywhere runs the program of that name, and is error 1 when there is none (it was error 1162) - DO name+"text" builds the program's name when the + is written against the name; with spaces round it the product calls it a syntax error (#1) The mock host can now hold program sources and load one when LoadProgram asks, which is how the golden gets its procedure files through the same path the IDE's host takes. The IDE's own "does not exist" message now uses the product's wording. SET PROCEDURE moves from "accepted and ignored" to "runs" in the language coverage map. Closes #3. Fixes #1. Co-Authored-By: Claude Opus 5.5 --- crates/foxvm/src/mock_host.rs | 19 ++ crates/foxvm/src/parser.rs | 44 +++++ crates/foxvm/src/value.rs | 2 + crates/foxvm/src/vm.rs | 181 +++++++++++++++++- .../foxvm/tests/fixtures/golden/fdvproca.prg | 19 ++ .../foxvm/tests/fixtures/golden/fdvprocb.prg | 12 ++ .../foxvm/tests/fixtures/golden/fdvrunme.prg | 6 + crates/foxvm/tests/golden.rs | 6 + crates/foxvm/tests/parse_program.rs | 2 +- crates/foxvm/tests/procedures.rs | 65 +++++++ .../tests/programs/set_procedure.expected | 30 +++ crates/foxvm/tests/programs/set_procedure.prg | 112 +++++++++++ docs/language-coverage.md | 10 +- src/renderer/src/runtime/session.ts | 8 +- tests/reference/coverage.test.ts | 1 + 15 files changed, 502 insertions(+), 15 deletions(-) create mode 100644 crates/foxvm/tests/fixtures/golden/fdvproca.prg create mode 100644 crates/foxvm/tests/fixtures/golden/fdvprocb.prg create mode 100644 crates/foxvm/tests/fixtures/golden/fdvrunme.prg create mode 100644 crates/foxvm/tests/procedures.rs create mode 100644 crates/foxvm/tests/programs/set_procedure.expected create mode 100644 crates/foxvm/tests/programs/set_procedure.prg diff --git a/crates/foxvm/src/mock_host.rs b/crates/foxvm/src/mock_host.rs index 5f52c14..b8ead0e 100644 --- a/crates/foxvm/src/mock_host.rs +++ b/crates/foxvm/src/mock_host.rs @@ -50,6 +50,10 @@ pub struct MockHost { rng: crate::host::SeededRandom, /// Upper-cased program name -> module id, for `resolve_program`. pub programs: HashMap, + /// Upper-cased program name -> source, for programs that are not loaded until something + /// asks: `run_with_requests` compiles one when the VM's `LoadProgram` names it, which is + /// what the IDE's host does with a `.prg` in the project. + pub program_sources: HashMap, /// Every request answered by `run_with_requests`, in order. pub requests: Vec, /// Handles released through `ReleaseObject`. @@ -118,6 +122,7 @@ impl MockHost { now: (crate::value::days_from_civil(2026, 9, 7), 12.0 * 3600.0 + 30.0 * 60.0), rng: crate::host::SeededRandom::default(), programs: HashMap::new(), + program_sources: HashMap::new(), requests: Vec::new(), released: Vec::new(), files: HashMap::new(), @@ -1230,6 +1235,20 @@ pub fn run_with_requests( Step::Done { value, .. } => return Ok(value), Step::Error(e) => return Err(e), Step::Suspend(req) => { + // a program the host holds the source of is compiled and loaded on request + if let HostRequest::LoadProgram { name } = &req + && let Some(src) = host.program_sources.get(&name.to_ascii_uppercase()).cloned() + { + let upper = name.to_ascii_uppercase(); + let module = compiler::compile_program(&src, &upper.to_ascii_lowercase()) + .module + .ok_or_else(|| RtError::syntax(format!("{upper} does not compile")))?; + let id = vm.load_module(module); + host.programs.insert(upper, id); + host.requests.push(req); + vm.resume(fiber, Value::number(id as f64)); + continue; + } let answer = match answers.pop_front() { Some(v) => { // Scripted answers still apply property writes so later reads see them. diff --git a/crates/foxvm/src/parser.rs b/crates/foxvm/src/parser.rs index 2c0b0d2..b7eef29 100644 --- a/crates/foxvm/src/parser.rs +++ b/crates/foxvm/src/parser.rs @@ -2418,6 +2418,17 @@ impl Parser { let (args, in_prog) = self.do_clauses()?; return Ok(Some(StmtKind::DoExpr { name, args, in_prog })); } + // `DO p_cod+"vx_prios.prg"`: measured, a name with a `+` right against it is an + // expression that builds the program's name, and the same line written with spaces + // round the `+` is a syntax error in the product - so only the unspaced form is read so + if let TokKind::Ident(_) = self.peek_kind() + && matches!(self.peek_at(1).kind, TokKind::Plus) + && self.peek_at(1).span.start == self.peek().span.end + { + let name = self.expr()?; + let (args, in_prog) = self.do_clauses()?; + return Ok(Some(StmtKind::DoExpr { name, args, in_prog })); + } let name = self.file_name("program name")?; let (args, in_prog) = self.do_clauses()?; Ok(Some(StmtKind::Do { name, args, in_prog })) @@ -6437,6 +6448,21 @@ impl Parser { flags: 0, })); } + // `RELEASE PROCEDURE a, b` takes those files off the SET PROCEDURE list. It is carried + // as a setting of its own name, which is where the list is kept. + if self.is_kw("PROCEDURE") && !self.peek_at(1).is_newline() && !matches!(self.peek_at(1).kind, TokKind::Eof) { + let tok = self.advance(); + let mut files = Vec::new(); + while !self.at_eol() { + files.push(self.file_name_word(tok.span, &[])?); + if !self.eat(&TokKind::Comma) { + break; + } + } + self.skip_line_keep_newline(); + let setting = Name::new("RELEASE PROCEDURE", first.span.to(tok.span)); + return Ok(Some(StmtKind::Set { setting, value: SetValue::To(files) })); + } const FORMS: &[&str] = &["LIBRARY", "PROCEDURE", "CLASSLIB"]; if let Some(word) = self.peek().ident().map(|s| s.to_string()) { let next_is_more = !self.peek_at(1).is_newline(); @@ -6613,6 +6639,24 @@ impl Parser { let args = vec![Expr::new(ExprKind::Bool(additive), span), named]; return Ok(StmtKind::Set { setting, value: SetValue::To(args) }); } + // `SET PROCEDURE TO a, b [ADDITIVE]`: the files are names, as a class library's are, and + // `SET PROCEDURE TO` with nothing after it empties the list + if kw_text(&setting.text, "PROCEDURE") && self.is_kw("TO") { + let span = setting.span; + self.advance(); + let mut files: Vec = Vec::new(); + while !self.at_eol() && !self.is_kw("ADDITIVE") { + files.push(self.file_name_word(span, &["ADDITIVE"])?); + if !self.eat(&TokKind::Comma) { + break; + } + } + let additive = self.eat_kw("ADDITIVE"); + self.skip_line_keep_newline(); + let mut args = vec![Expr::new(ExprKind::Bool(additive), span)]; + args.append(&mut files); + return Ok(StmtKind::Set { setting, value: SetValue::To(args) }); + } if kw_text(&setting.text, "CLASSLIB") { let span = setting.span; let _ = self.eat_kw("TO"); diff --git a/crates/foxvm/src/value.rs b/crates/foxvm/src/value.rs index 6bf5297..803dd92 100644 --- a/crates/foxvm/src/value.rs +++ b/crates/foxvm/src/value.rs @@ -322,6 +322,8 @@ pub const REMEMBERED: &[(&str, SettingShape, &str)] = &[ ("BRSTATUS", SettingShape::Switch, "OFF"), ("CARRY", SettingShape::Switch, "OFF"), ("CLASSLIB", SettingShape::Word, ""), + // the list itself is the VM's; this is only what SET() answers when nothing is on it + ("PROCEDURE", SettingShape::Word, ""), ("CLEAR", SettingShape::Switch, "ON"), ("CLOCK", SettingShape::Switch, "OFF"), ("COLLATE", SettingShape::Word, "MACHINE"), diff --git a/crates/foxvm/src/vm.rs b/crates/foxvm/src/vm.rs index d15b58c..1c809d6 100644 --- a/crates/foxvm/src/vm.rs +++ b/crates/foxvm/src/vm.rs @@ -92,6 +92,8 @@ enum Pending { func: Option, args: Vec, discard: bool, + /// The program the host was asked for, for the error when it has none. + program: String, }, /// QUIT / CANCEL: the fiber ends when resumed. Finish, @@ -144,6 +146,14 @@ struct TryHandler { pending_len: usize, } +/// The files of a `SET PROCEDURE TO`, and how far loading them has got. +#[derive(Debug, Default)] +struct ProcedureLoad { + additive: bool, + names: Vec, + next: usize, +} + #[derive(Debug, Default)] struct Fiber { frames: Vec, @@ -164,6 +174,9 @@ struct Fiber { /// Whether that reply is a `SET CLASSLIB` the host has read rather than an index file, so /// the instruction that runs again knows which of the two it was waiting for. classlib_reply: bool, + /// A `SET PROCEDURE TO` part way through loading its files: each one the host has to + /// load is a round trip, and the list only changes once every file is in. + procedure_load: Option, /// What the debugger asked this fiber to do when it was let go, and how deep its frames /// were then - which is what tells a step over a call from a step into it. stepping: Option<(StepMode, usize)>, @@ -253,6 +266,13 @@ pub struct Vm { /// The functions those libraries added: the name a program calls, and which function of /// which library it is. A name loaded twice is the later one, as the product's is. library_funcs: HashMap, + /// `SET PROCEDURE TO`: the programs whose routines a bare call finds after the current + /// program's own, in the order they are looked in - which is the order they were listed. + procedure_files: Vec, + /// Programs loaded only to be procedure files and never run. Measured: once a procedure + /// file is released its routines are not found, while a program that ran with `DO` keeps + /// its routines findable after it returns. These are the ones that go when released. + procedure_only: HashSet, /// Objects whose `Error` method is running, so an error inside it is not handed back to it. in_class_error: HashSet, /// The work area the last USE opened, so the index beside it can be read next. @@ -1538,6 +1558,8 @@ impl Vm { dlls: HashMap::new(), libraries: Vec::new(), library_funcs: HashMap::new(), + procedure_files: Vec::new(), + procedure_only: HashSet::new(), last_opened: None, index_build: None, idx_open: None, @@ -2404,13 +2426,18 @@ impl Vm { fb.stack.push(it.next().unwrap_or(Value::Null)); } } - Some(Pending::LoadedProgram { func, args, discard }) => { + Some(Pending::LoadedProgram { func, args, discard, program }) => { let id = match v.deref() { Value::Number(n, ..) if n >= 0.0 && (n as usize) < self.modules.len() => n as u32, - _ => return Err(RtError::procedure_not_found(func.as_deref().unwrap_or("MAIN"))), + // measured: a program that is not there is error 1, "File 'x.prg' does not + // exist.", whether DO or a bare call went looking for it + _ => return Err(program_missing(&program)), }; let fidx = match &func { - None => 0, + None => { + self.procedure_only.remove(&id); + 0 + } Some(name) => { self.modules[id as usize].find_func(name).ok_or_else(|| RtError::procedure_not_found(name))? } @@ -2942,9 +2969,17 @@ impl Vm { if let Some(f) = self.modules[current as usize].find_func(upper) { return Some((current, f)); } + // measured: the procedure files come next, in the order SET PROCEDURE listed them, + // ahead of any other program - a routine in two of them is the first file's + for &id in &self.procedure_files { + if let Some(f) = self.modules[id as usize].find_func(upper) { + return Some((id, f)); + } + } for (i, m) in self.modules.iter().enumerate().rev() { if i as u32 != current && m.kind == ModuleKind::Program + && !self.procedure_only.contains(&(i as u32)) && let Some(f) = m.find_func(upper) { return Some((i as u32, f)); @@ -3555,9 +3590,19 @@ impl Vm { fb.stack.push(v); return Ok(Flow::Next); } - let (m, f) = - self.find_function(current, upper).ok_or_else(|| RtError::procedure_not_found(upper))?; - self.push_call(fb, m, f, None, args, FrameKind::Call { discard: false }, true)?; + // measured: a name that is no routine anywhere is looked for as a program + // of that name, which runs and answers with what it returns - and when there + // is none, it is error 1 as a DO of it would be + if let Some((m, f)) = self.find_function(current, upper) { + self.push_call(fb, m, f, None, args, FrameKind::Call { discard: false }, true)?; + } else if let Some(id) = self.find_program(upper).or_else(|| host.resolve_program(upper)) { + self.procedure_only.remove(&id); + self.push_call(fb, id, 0, None, args, FrameKind::Call { discard: false }, true)?; + } else { + fb.pending = + Some(Pending::LoadedProgram { func: None, args, discard: false, program: upper.to_string() }); + return Ok(Flow::Suspend(HostRequest::LoadProgram { name: upper.to_string() })); + } } } Instr::CallBuiltin { id, argc } => { @@ -3941,6 +3986,11 @@ impl Vm { })); } Instr::SetCmd { name, argc, to } => { + // SET PROCEDURE comes back here once per file the host had to load + if fb.procedure_load.is_some() && fb.data_reply.is_some() { + let reply = fb.data_reply.take(); + return self.continue_procedure_load(host, fb, reply); + } // SET INDEX has files to read, so it comes back here once per file with the // bytes of one; every other setting is answered where it stands if let Some(answer) = fb.data_reply.take() { @@ -3965,6 +4015,27 @@ impl Vm { // A class library is a file the host reads, which it cannot do while the VM // is on the stack, so this one waits where `SET LIBRARY TO` - whose own file // is loaded through the host in `set_cmd` - does not. + if setting == "RELEASE PROCEDURE" { + let names = args.iter().map(|v| v.as_str().map(|s| s.to_string())).collect::, _>>()?; + self.release_procedures(&names); + return Ok(Flow::Next); + } + // `SET PROCEDURE TO a, b [ADDITIVE]`: the additive flag, then the files + if setting == "PROCEDURE" { + let additive = match args.first() { + Some(v) => v.truthy()?, + None => false, + }; + let mut names = Vec::new(); + for v in args.iter().skip(1) { + let name = v.as_str()?.trim().to_string(); + if !name.is_empty() { + names.push(name); + } + } + fb.procedure_load = Some(ProcedureLoad { additive, names, ..Default::default() }); + return self.continue_procedure_load(host, fb, None); + } if setting == "CLASSLIB" { let request = self.classlib_request(&args)?; fb.classlib_reply = true; @@ -5787,6 +5858,80 @@ impl Vm { /// `DO name [IN prog]`: a procedure of the running module, an already loaded program, or /// one the host has to find and compile first. + /// Loads the files of a `SET PROCEDURE TO` one at a time, asking the host for each one it + /// does not already have. + /// + /// Measured: without ADDITIVE the list is emptied before anything is loaded, and each file + /// goes on as it is found - so `SET PROCEDURE TO a, missing, b` raises error 1 and leaves + /// `a` on the list and nothing else. With ADDITIVE a missing file leaves the list as it was. + fn continue_procedure_load(&mut self, host: &mut dyn Host, fb: &mut Fiber, reply: Option) -> Result { + let mut state = fb.procedure_load.take().unwrap_or_default(); + match reply { + None => { + if !state.additive { + self.procedure_files.clear(); + self.remember_procedures(); + } + } + Some(v) => match v.deref() { + Value::Number(n, ..) if n >= 0.0 && (n as usize) < self.modules.len() => { + self.procedure_only.insert(n as u32); + self.add_procedure_file(n as u32); + state.next += 1; + } + _ => return Err(program_missing(&state.names[state.next])), + }, + } + while let Some(name) = state.names.get(state.next) { + let stem = program_stem(name); + if let Some(id) = self.find_program(&stem) { + self.add_procedure_file(id); + } else if let Some(id) = host.resolve_program(&stem) { + self.procedure_only.insert(id); + self.add_procedure_file(id); + } else { + let request = HostRequest::LoadProgram { name: stem }; + fb.procedure_load = Some(state); + return Ok(self.ask(fb, request)); + } + state.next += 1; + } + Ok(Flow::Next) + } + + /// Puts a program on the end of the procedure list. Measured: one that is already on it + /// stays where it is rather than moving to the end. + fn add_procedure_file(&mut self, id: u32) { + if !self.procedure_files.contains(&id) { + self.procedure_files.push(id); + self.remember_procedures(); + } + } + + /// `RELEASE PROCEDURE a, b`: those files come off the list. One that is not on it is + /// nothing to release. + fn release_procedures(&mut self, names: &[String]) { + let stems: Vec = names.iter().map(|n| program_stem(n)).collect(); + let modules = &self.modules; + self.procedure_files.retain(|&id| !stems.iter().any(|s| program_stem(&modules[id as usize].name) == *s)); + self.remember_procedures(); + } + + /// What `SET("PROCEDURE")` answers: every file on the list, quoted, as the compiled program + /// it is loaded from - the full path of the `.FXP` - separated by a comma and a space. + fn remember_procedures(&mut self) { + let text = self + .procedure_files + .iter() + .map(|&id| { + let stem = program_stem(&self.modules[id as usize].name); + format!("\"{}\"", self.settings.at(&format!("{stem}.FXP")).to_ascii_uppercase()) + }) + .collect::>() + .join(", "); + self.settings.remembered.insert("PROCEDURE".to_string(), text); + } + fn run_do( &mut self, fb: &mut Fiber, @@ -5801,9 +5946,11 @@ impl Vm { if let Some((m, f)) = self.find_function(current, &upper) { self.push_call(fb, m, f, None, args, FrameKind::Call { discard: true }, true)?; } else if let Some(id) = self.find_program(&upper).or_else(|| host.resolve_program(&upper)) { + // a program that has run keeps its routines findable, procedure file or not + self.procedure_only.remove(&id); self.push_call(fb, id, 0, None, args, FrameKind::Call { discard: true }, true)?; } else { - fb.pending = Some(Pending::LoadedProgram { func: None, args, discard: true }); + fb.pending = Some(Pending::LoadedProgram { func: None, args, discard: true, program: upper.clone() }); return Ok(Flow::Suspend(HostRequest::LoadProgram { name: upper })); } } @@ -5813,7 +5960,7 @@ impl Vm { self.push_call(fb, id, f, None, args, FrameKind::Call { discard: true }, true)?; } None => { - fb.pending = Some(Pending::LoadedProgram { func: Some(upper), args, discard: true }); + fb.pending = Some(Pending::LoadedProgram { func: Some(upper), args, discard: true, program: prog.clone() }); return Ok(Flow::Suspend(HostRequest::LoadProgram { name: prog })); } }, @@ -10018,6 +10165,24 @@ fn same_value(stored: Option<&crate::dbf::DbfValue>, value: &Value, settings: &S } /// The work area an `IN` clause named: a number, or an alias. +/// A program as a procedure file names it: the file's stem, without its folder or extension. +fn program_stem(name: &str) -> String { + let file = name.trim().rsplit(['\\', '/']).next().unwrap_or(name).to_string(); + let lower = file.to_ascii_lowercase(); + let stem = if lower.ends_with(".prg") || lower.ends_with(".fxp") { &file[..file.len() - 4] } else { file.as_str() }; + stem.to_ascii_uppercase() +} + +/// Error 1 for a procedure file that is not there, in the product's words: the name as the +/// program wrote it, given the extension of a program when it had none. +fn program_missing(name: &str) -> RtError { + let file = name.trim().rsplit(['\\', '/']).next().unwrap_or(name).to_string(); + // a name the compiler upper-cased is given back as the product writes it, in lower case + let file = if file == file.to_ascii_uppercase() { file.to_ascii_lowercase() } else { file }; + let file = if file.contains('.') { file } else { format!("{file}.prg") }; + RtError::new(RtError::FILE_NOT_FOUND, format!("File '{file}' does not exist.")) +} + fn area_ref(v: &Value) -> Result { Ok(match v.deref() { Value::Str(s) => AreaRef::Alias(s.trim().to_string()), diff --git a/crates/foxvm/tests/fixtures/golden/fdvproca.prg b/crates/foxvm/tests/fixtures/golden/fdvproca.prg new file mode 100644 index 0000000..de2212e --- /dev/null +++ b/crates/foxvm/tests/fixtures/golden/fdvproca.prg @@ -0,0 +1,19 @@ +* A procedure file for the set_procedure golden. SET PROCEDURE never runs this line. +? "fdvproca main ran" + +PROCEDURE fdvhello + ? "hello from fdvproca" +ENDPROC + +FUNCTION fdvboth + RETURN "fdvboth in fdvproca" +ENDFUNC + +FUNCTION fdvonlya + * a routine of the same file is found first, whatever else is loaded + RETURN "fdvonlya calls " + fdvsibling() +ENDFUNC + +FUNCTION fdvsibling + RETURN "its sibling in fdvproca" +ENDFUNC diff --git a/crates/foxvm/tests/fixtures/golden/fdvprocb.prg b/crates/foxvm/tests/fixtures/golden/fdvprocb.prg new file mode 100644 index 0000000..135dd56 --- /dev/null +++ b/crates/foxvm/tests/fixtures/golden/fdvprocb.prg @@ -0,0 +1,12 @@ +* A second procedure file for the set_procedure golden. +FUNCTION fdvboth + RETURN "fdvboth in fdvprocb" +ENDFUNC + +FUNCTION fdvonlyb + RETURN "fdvonlyb in fdvprocb" +ENDFUNC + +FUNCTION fdvmine + RETURN "fdvmine in fdvprocb" +ENDFUNC diff --git a/crates/foxvm/tests/fixtures/golden/fdvrunme.prg b/crates/foxvm/tests/fixtures/golden/fdvrunme.prg new file mode 100644 index 0000000..a5abe6b --- /dev/null +++ b/crates/foxvm/tests/fixtures/golden/fdvrunme.prg @@ -0,0 +1,6 @@ +* A program the set_procedure golden runs with DO: its routines stay findable afterwards. +? "fdvrunme ran" + +FUNCTION fdvinrunme + RETURN "fdvinrunme after it returned" +ENDFUNC diff --git a/crates/foxvm/tests/golden.rs b/crates/foxvm/tests/golden.rs index 92125c2..a75290f 100644 --- a/crates/foxvm/tests/golden.rs +++ b/crates/foxvm/tests/golden.rs @@ -65,6 +65,12 @@ fn run(src: &str, host: &mut MockHost) -> Result, RtError> { let other = compile_program(&other_src, "other").module.expect("other compiles"); let other_id = vm.load_module(other); host.programs.insert("OTHER".into(), other_id); + // the stage's other programs are not loaded until something asks for one, as a `.prg` in a + // project is not: SET PROCEDURE and DO load them through the host + for name in ["fdvproca", "fdvprocb", "fdvrunme"] { + let src = fs::read_to_string(fixtures_dir().join(format!("{name}.prg"))).expect("a procedure fixture"); + host.program_sources.insert(name.to_ascii_uppercase(), src); + } // a report file to run, so a golden program can print one let (frx, frt) = dbf_fixture::sample(); host.files.insert("PARTS.FRX".into(), frx); diff --git a/crates/foxvm/tests/parse_program.rs b/crates/foxvm/tests/parse_program.rs index 5e0c406..8c3c29b 100644 --- a/crates/foxvm/tests/parse_program.rs +++ b/crates/foxvm/tests/parse_program.rs @@ -343,7 +343,7 @@ fn set_statements() { assert_eq!(ok("SET PATH TO \"a;b\", \"c\""), "(set PATH (to \"a;b\"))"); assert_eq!(ok("SET PATH TO (lcPath)"), "(set PATH (to LCPATH))"); assert_eq!(ok("SET PATH TO"), "(set PATH (word \"\"))"); - assert_eq!(ok("SET PROCEDURE TO"), "(set PROCEDURE (word \"\"))"); + assert_eq!(ok("SET PROCEDURE TO"), "(set PROCEDURE (to .F.))"); assert_eq!(ok("SET STATUS BAR ON"), "(set STATUS BAR on)"); assert_eq!(ok("SET SYSMENU TO DEFAULT"), "(menu SetSysMenu)"); assert_eq!(ok("SET STEP"), "(set STEP (word \"\"))"); diff --git a/crates/foxvm/tests/procedures.rs b/crates/foxvm/tests/procedures.rs new file mode 100644 index 0000000..5ba228a --- /dev/null +++ b/crates/foxvm/tests/procedures.rs @@ -0,0 +1,65 @@ +//! `SET PROCEDURE`, `RELEASE PROCEDURE`, and where a bare call or `DO` finds its routine. The +//! behaviour is measured against Visual FoxPro 9 in the `set_procedure` golden; these pin the +//! parse shapes and the load path a host takes. + +mod common; + +use foxvm::mock_host::{MockHost, run_program}; +use foxvm::host::HostRequest; + +fn host() -> MockHost { + let mut host = MockHost::new(); + host.program_sources.insert("LIBA".into(), "? 'liba main ran'\nFUNCTION greet\nRETURN 'greet in liba'\n".into()); + host.program_sources.insert("LIBB".into(), "FUNCTION greet\nRETURN 'greet in libb'\n".into()); + host.program_sources.insert("TOOL".into(), "LPARAMETERS n\nRETURN n * 2\n".into()); + host +} + +fn run(src: &str) -> (Vec, MockHost) { + let mut host = host(); + match run_program(src, &mut host) { + Ok((_, output)) => (output, host), + Err(e) => panic!("{} at line {}: {}", e.code, e.line, e.message), + } +} + +#[test] +fn set_procedure_parses_its_files_and_additive() { + assert_eq!(common::ok("SET PROCEDURE TO a, b ADDITIVE"), "(set PROCEDURE (to .T., \"a\", \"b\"))"); + assert_eq!(common::ok("SET PROCEDURE TO"), "(set PROCEDURE (to .F.))"); + assert_eq!(common::ok("SET PROCEDURE TO ..\\common50\\codemine"), "(set PROCEDURE (to .F., \"..\\\\common50\\\\codemine\"))"); + assert_eq!(common::ok("RELEASE PROCEDURE a, b"), "(set RELEASE PROCEDURE (to \"a\", \"b\"))"); +} + +#[test] +fn do_reads_a_name_built_right_against_a_plus() { + assert_eq!(common::ok("DO p_cod+\"vx_prios.prg\" WITH 1"), "(do-expr (+ P_COD \"vx_prios.prg\") (with 1))"); + // measured: with spaces round the `+` the product calls it a syntax error + assert!(!foxvm::parser::parse_program("DO p_cod + \"vx_prios\"").diagnostics.is_empty()); +} + +#[test] +fn a_procedure_file_is_loaded_through_the_host_and_its_main_does_not_run() { + let (out, host) = run("SET PROCEDURE TO liba\n? greet()"); + assert_eq!(out, ["greet in liba"]); + assert!(host.requests.iter().any(|r| matches!(r, HostRequest::LoadProgram { name } if name == "LIBA"))); +} + +#[test] +fn a_released_file_is_not_searched_but_a_program_that_ran_is() { + let (out, _) = run("SET PROCEDURE TO liba\nSET PROCEDURE TO libb\n? greet()\nRELEASE PROCEDURE libb\nTRY\n x = greet()\nCATCH TO e\n ? e.ErrorNo\nENDTRY"); + assert_eq!(out, ["greet in libb", " 1"]); +} + +#[test] +fn a_call_to_no_routine_runs_the_program_of_that_name() { + let (out, _) = run("? TRANSFORM(tool(21))"); + assert_eq!(out, ["42"]); +} + +#[test] +fn a_call_to_nothing_at_all_is_error_1_naming_the_file() { + let mut host = host(); + let e = run_program("x = nosuch()", &mut host).err().expect("an error"); + assert_eq!((e.code, e.message.as_str()), (1, "File 'nosuch.prg' does not exist.")); +} diff --git a/crates/foxvm/tests/programs/set_procedure.expected b/crates/foxvm/tests/programs/set_procedure.expected new file mode 100644 index 0000000..9ca3882 --- /dev/null +++ b/crates/foxvm/tests/programs/set_procedure.expected @@ -0,0 +1,30 @@ +none [] +one [FDVPROCA] +extension FXP +hello from fdvproca +fdvboth in fdvproca +fdvonlya calls its sibling in fdvproca +additive [FDVPROCA FDVPROCB] +fdvboth in fdvproca +fdvonlyb in fdvprocb +fdvmine in main +replaced [FDVPROCB] +fdvboth in fdvprocb +gone 1 File 'fdvhello.prg' does not exist. +two [FDVPROCA FDVPROCB] fdvboth in fdvproca +other order [FDVPROCB FDVPROCA] fdvboth in fdvprocb +again [FDVPROCB FDVPROCA] +released [FDVPROCA] fdvboth in fdvproca +emptied [] +not found 1 File 'fdvboth.prg' does not exist. +missing 1 File 'fdvnosuch.prg' does not exist. +after missing [] +missing second 1 File 'fdvnosuch.prg' does not exist. +after missing second [FDVPROCA] +missing additive 1 File 'fdvnosuch.prg' does not exist. +after missing additive [FDVPROCA] +expression [FDVPROCB] +macro [FDVPROCB] +with extension [FDVPROCA] +fdvrunme ran +fdvinrunme after it returned diff --git a/crates/foxvm/tests/programs/set_procedure.prg b/crates/foxvm/tests/programs/set_procedure.prg new file mode 100644 index 0000000..707dd17 --- /dev/null +++ b/crates/foxvm/tests/programs/set_procedure.prg @@ -0,0 +1,112 @@ +* COVERS: SET PROCEDURE, RELEASE PROCEDURE, SET, DO +* +* Where a bare call finds its routine: the program it is written in, then the procedure files +* in the order SET PROCEDURE listed them, then the programs that have run. Every answer below was +* measured in Visual FoxPro 9. SET("PROCEDURE") is printed as the files' stems, because what it +* really answers is the full path of each .FXP, and that is wherever the program ran. +? "none" + " " + procs() +SET PROCEDURE TO fdvproca +? "one" + " " + procs() +? "extension" + " " + JUSTEXT(CHRTRAN(SET("PROCEDURE"), '"', "")) + +* the file's own first line never runs; its routines are found by DO and by a call +DO fdvhello +? fdvboth() +? fdvonlya() + +* ADDITIVE puts a file on the end, and the first file listed wins a name both have; the +* program's own routine beats every procedure file +SET PROCEDURE TO fdvprocb ADDITIVE +? "additive" + " " + procs() +? fdvboth() +? fdvonlyb() +? fdvmine() + +* without ADDITIVE the list is replaced, and a routine of a file that went is not found +SET PROCEDURE TO fdvprocb +? "replaced" + " " + procs() +? fdvboth() +TRY + lcGot = fdvhello() +CATCH TO loErr + ? "gone" + " " + TRANSFORM(loErr.ErrorNo) + " " + loErr.Message +ENDTRY + +* several at once, in the order written +SET PROCEDURE TO fdvproca, fdvprocb +? "two" + " " + procs() + " " + fdvboth() +SET PROCEDURE TO fdvprocb, fdvproca +? "other order" + " " + procs() + " " + fdvboth() + +* a file already listed stays where it is +SET PROCEDURE TO fdvproca ADDITIVE +? "again" + " " + procs() + +* RELEASE PROCEDURE takes one off; SET PROCEDURE TO on its own empties the list +RELEASE PROCEDURE fdvprocb +? "released" + " " + procs() + " " + fdvboth() +SET PROCEDURE TO +? "emptied" + " " + procs() +TRY + lcGot = fdvboth() +CATCH TO loErr + ? "not found" + " " + TRANSFORM(loErr.ErrorNo) + " " + loErr.Message +ENDTRY + +* a file that is not there is error 1; without ADDITIVE the list is emptied first, so what was +* on it is gone, and a file listed before the missing one is on it +SET PROCEDURE TO fdvproca +TRY + SET PROCEDURE TO fdvnosuch +CATCH TO loErr + ? "missing" + " " + TRANSFORM(loErr.ErrorNo) + " " + loErr.Message +ENDTRY +? "after missing" + " " + procs() +TRY + SET PROCEDURE TO fdvproca, fdvnosuch, fdvprocb +CATCH TO loErr + ? "missing second" + " " + TRANSFORM(loErr.ErrorNo) + " " + loErr.Message +ENDTRY +? "after missing second" + " " + procs() +TRY + SET PROCEDURE TO fdvnosuch ADDITIVE +CATCH TO loErr + ? "missing additive" + " " + TRANSFORM(loErr.ErrorNo) + " " + loErr.Message +ENDTRY +? "after missing additive" + " " + procs() + +* the name may be worked out +lcLib = "fdvprocb" +SET PROCEDURE TO (lcLib) +? "expression" + " " + procs() +SET PROCEDURE TO &lcLib ADDITIVE +? "macro" + " " + procs() +SET PROCEDURE TO fdvproca.prg +? "with extension" + " " + procs() + +* a program that ran with DO keeps its routines findable after it returns, and a program's +* name may be built right against a `+` +SET PROCEDURE TO +lcPre = "fdv" +DO lcPre+"runme" +? fdvinrunme() +RETURN + +FUNCTION fdvmine + RETURN "fdvmine in main" +ENDFUNC + +* the stems of the files on the list, in order +FUNCTION procs + LOCAL lcList, lcOut, lnCount, lnI + lcList = SET("PROCEDURE") + IF EMPTY(lcList) + RETURN "[]" + ENDIF + lcOut = "" + lnCount = GETWORDCOUNT(lcList, ",") + FOR lnI = 1 TO lnCount + lcOut = lcOut + IIF(lnI > 1, " ", "") + JUSTSTEM(CHRTRAN(ALLTRIM(GETWORDNUM(lcList, lnI, ",")), '"', "")) + ENDFOR + RETURN "[" + lcOut + "]" +ENDFUNC diff --git a/docs/language-coverage.md b/docs/language-coverage.md index 651380b..2934ed2 100644 --- a/docs/language-coverage.md +++ b/docs/language-coverage.md @@ -20,7 +20,7 @@ Every element of the Visual FoxPro language, and what happens when a program use | Kind | Runs | Accepted and ignored | Answered | Refused, by name | Unknown | Total | |---|---:|---:|---:|---:|---:|---:| -| command | 334 | 76 | 4 | 19 | 0 | 433 | +| command | 335 | 75 | 4 | 19 | 0 | 433 | | function | 411 | 0 | 0 | 15 | 0 | 426 | | property | 481 | 0 | 0 | 0 | 3 | 484 | | method | 105 | 0 | 0 | 1 | 0 | 106 | @@ -34,13 +34,13 @@ Every element of the Visual FoxPro language, and what happens when a program use ## command -### Runs (334) +### Runs (335) -**!**, **?**, **??**, ???, **@ ... BOX**, **@ ... CLASS**, **@ ... CLEAR**, **@ ... EDIT - Edit Boxes**, **@ ... FILL**, **@ ... GET - Check Boxes**, **@ ... GET - Combo Boxes**, **@ ... GET - Command Buttons**, **@ ... GET - List Boxes**, **@ ... GET - Option Buttons**, **@ ... GET - Spinners**, **@ ... GET - Text Boxes**, **@ ... GET - Transparent Buttons**, **@ ... MENU**, **@ ... PROMPT**, **@ ... SAY**, **@ ... SAY - Pictures & OLE Objects**, **@ ... SCROLL**, **@ ... TO**, *****, **\**, **\\**, **&**, **&&**, **=**, **ACTIVATE MENU**, **ACTIVATE POPUP**, **ACTIVATE SCREEN**, **ACTIVATE WINDOW**, **ADD TABLE**, **ALTER TABLE - SQL**, **APPEND**, **APPEND FROM**, **APPEND FROM ARRAY**, **APPEND GENERAL**, **APPEND MEMO**, **APPEND PROCEDURES**, **ASSERT**, **AVERAGE**, **BEGIN TRANSACTION**, **BLANK**, BROWSE, **BUILD APP**, **BUILD DLL**, **BUILD EXE**, BUILD MTDLL, **BUILD PROJECT**, **CALCULATE**, **CANCEL**, **CD**, **CHANGE**, **CHDIR**, **CLEAR**, **CLOSE MEMO**, **COMPILE**, **CONTINUE**, **COPY FILE**, **COPY INDEXES**, **COPY MEMO**, **COPY PROCEDURES**, **COPY STRUCTURE**, **COPY STRUCTURE EXTENDED**, **COPY TAG**, **COPY TO**, **COPY TO ARRAY**, **COUNT**, **CREATE CONNECTION**, **CREATE CURSOR - SQL**, **CREATE DATABASE**, **CREATE FORM**, **CREATE FROM**, **CREATE MENU**, CREATE PROJECT, **CREATE QUERY**, **CREATE SCREEN**, **CREATE SCREEN - Quick Screen**, **CREATE SQL VIEW**, **CREATE TABLE - SQL**, CREATE VIEW, **DEACTIVATE MENU**, **DEACTIVATE POPUP**, **DEACTIVATE WINDOW**, DEBUG, **DEBUGOUT**, **DECLARE**, **DECLARE - DLL**, **DEFINE BAR**, **DEFINE CLASS**, **DEFINE MENU**, **DEFINE PAD**, **DEFINE POPUP**, **DEFINE WINDOW**, **DELETE**, **DELETE - SQL**, **DELETE CONNECTION**, **DELETE DATABASE**, **DELETE FILE**, **DELETE TAG**, **DELETE TRIGGER**, **DELETE VIEW**, **DIMENSION**, DIR or DIRECTORY, **DISPLAY**, DISPLAY CONNECTIONS, DISPLAY DATABASE, DISPLAY DLLS, DISPLAY FILES, **DISPLAY MEMORY**, DISPLAY OBJECTS, DISPLAY PROCEDURES, **DISPLAY STATUS**, **DISPLAY STRUCTURE**, DISPLAY TABLES, DISPLAY VIEWS, **DO**, **DO CASE ... ENDCASE**, DO FORM, **DO WHILE ... ENDDO**, **DOEVENTS**, **DROP TABLE**, **DROP VIEW**, **EDIT**, **EJECT**, EJECT PAGE, **END TRANSACTION**, **ERASE**, **ERROR**, **EXIT**, **EXPORT**, **EXTERNAL**, **FIND**, **FLUSH**, **FOR ... ENDFOR**, **FOR EACH ... ENDFOR**, **FREE TABLE**, **FUNCTION**, **GATHER**, **GETEXPR**, **GO**, **GOTO**, **HIDE MENU**, **HIDE POPUP**, **HIDE WINDOW**, **IF ... ENDIF**, **IMPORT**, **INDEX**, **INPUT**, **INSERT**, **INSERT - SQL**, **JOIN**, **KEYBOARD**, **LIST**, **LIST CONNECTIONS**, **LIST DATABASE**, LIST DLLS, LIST OBJECTS, **LIST PROCEDURES**, LIST TABLES, LIST VIEWS, **LOCAL**, **LOCATE**, **LOOP**, **LPARAMETERS**, **MD**, **MENU**, **MENU TO**, **MKDIR**, MODIFY CLASS, MODIFY COMMAND, **MODIFY CONNECTION**, MODIFY DATABASE, MODIFY FILE, MODIFY FORM, MODIFY LABEL, **MODIFY MEMO**, MODIFY MENU, MODIFY PROCEDURE, MODIFY PROJECT, MODIFY QUERY, MODIFY REPORT, **MODIFY STRUCTURE**, MODIFY VIEW, **MODIFY WINDOW**, **MOUSE**, **MOVE POPUP**, **MOVE WINDOW**, **NOTE**, **ON BAR**, **ON ERROR**, **ON ESCAPE**, **ON EXIT BAR**, **ON EXIT MENU**, **ON EXIT PAD**, **ON EXIT POPUP**, **ON KEY**, **ON KEY =**, **ON KEY LABEL**, **ON PAD**, **ON PAGE**, **ON READERROR**, **ON SELECTION BAR**, **ON SELECTION MENU**, **ON SELECTION PAD**, **ON SELECTION POPUP**, **ON SHUTDOWN**, **OPEN DATABASE**, **PACK**, **PACK DATABASE**, **PARAMETERS**, **POP KEY**, **POP MENU**, **POP POPUP**, **PRINTJOB ... ENDPRINTJOB**, **PRIVATE**, **PROCEDURE**, **PUBLIC**, **PUSH KEY**, **PUSH MENU**, **PUSH POPUP**, QUIT, **RD**, **READ**, READ EVENTS, READ MENU, **RECALL**, **REINDEX**, **RELEASE**, **RELEASE BAR**, RELEASE CLASSLIB, RELEASE LIBRARY, **RELEASE MENUS**, **RELEASE PAD**, **RELEASE POPUPS**, **RELEASE PROCEDURE**, **RELEASE WINDOWS**, **REMOVE TABLE**, **RENAME**, RENAME CLASS, **RENAME CONNECTION**, **RENAME TABLE**, **RENAME VIEW**, **REPLACE**, **REPLACE FROM ARRAY**, **REPORT FORM**, **RESTORE FROM**, **RESTORE SCREEN**, **RESTORE WINDOW**, **RESUME**, **RETRY**, **RETURN**, **RMDIR**, **ROLLBACK**, **RUN**, **SAVE SCREEN**, **SAVE TO**, **SAVE WINDOWS**, **SCAN ... ENDSCAN**, **SCATTER**, **SCROLL**, **SEEK**, **SELECT**, **SELECT - SQL**, **SET ASSERTS**, **SET CENTURY**, **SET CURRENCY**, **SET DATABASE**, **SET DATE**, **SET DEBUG**, **SET DEBUGOUT**, **SET DECIMALS**, **SET DEFAULT**, **SET DELETED**, **SET ECHO**, **SET ESCAPE**, **SET EXACT**, **SET FDOW**, **SET FILTER**, **SET FIXED**, **SET FWEEK**, **SET HEADINGS**, **SET HOURS**, **SET INDEX**, **SET KEY**, **SET LIBRARY**, **SET MARK OF**, **SET MARK TO**, **SET MEMOWIDTH**, **SET MESSAGE**, **SET NEAR**, **SET NULLDISPLAY**, **SET ORDER**, **SET PATH**, **SET POINT**, **SET RELATION**, **SET RELATION OFF**, **SET SAFETY**, **SET SECONDS**, **SET SEPARATOR**, **SET SKIP OF**, **SET SYSMENU**, **SET TALK**, **SET TEXTMERGE**, **SET TEXTMERGE DELIMITERS**, **SET UNIQUE**, **SHOW GET**, **SHOW GETS**, **SHOW MENU**, **SHOW OBJECT**, **SHOW POPUP**, **SHOW WINDOW**, **SIZE POPUP**, **SIZE WINDOW**, **SKIP**, **SORT**, **STORE**, **SUM**, **SUSPEND**, **TEXT ... ENDTEXT**, **TOTAL**, **TRY...CATCH...FINALLY**, **TYPE**, **UNLOCK**, UPDATE, **UPDATE - SQL**, **USE**, **VALIDATE DATABASE**, **WAIT**, **WITH ... ENDWITH**, **ZAP**, **ZOOM WINDOW** +**!**, **?**, **??**, ???, **@ ... BOX**, **@ ... CLASS**, **@ ... CLEAR**, **@ ... EDIT - Edit Boxes**, **@ ... FILL**, **@ ... GET - Check Boxes**, **@ ... GET - Combo Boxes**, **@ ... GET - Command Buttons**, **@ ... GET - List Boxes**, **@ ... GET - Option Buttons**, **@ ... GET - Spinners**, **@ ... GET - Text Boxes**, **@ ... GET - Transparent Buttons**, **@ ... MENU**, **@ ... PROMPT**, **@ ... SAY**, **@ ... SAY - Pictures & OLE Objects**, **@ ... SCROLL**, **@ ... TO**, *****, **\**, **\\**, **&**, **&&**, **=**, **ACTIVATE MENU**, **ACTIVATE POPUP**, **ACTIVATE SCREEN**, **ACTIVATE WINDOW**, **ADD TABLE**, **ALTER TABLE - SQL**, **APPEND**, **APPEND FROM**, **APPEND FROM ARRAY**, **APPEND GENERAL**, **APPEND MEMO**, **APPEND PROCEDURES**, **ASSERT**, **AVERAGE**, **BEGIN TRANSACTION**, **BLANK**, BROWSE, **BUILD APP**, **BUILD DLL**, **BUILD EXE**, BUILD MTDLL, **BUILD PROJECT**, **CALCULATE**, **CANCEL**, **CD**, **CHANGE**, **CHDIR**, **CLEAR**, **CLOSE MEMO**, **COMPILE**, **CONTINUE**, **COPY FILE**, **COPY INDEXES**, **COPY MEMO**, **COPY PROCEDURES**, **COPY STRUCTURE**, **COPY STRUCTURE EXTENDED**, **COPY TAG**, **COPY TO**, **COPY TO ARRAY**, **COUNT**, **CREATE CONNECTION**, **CREATE CURSOR - SQL**, **CREATE DATABASE**, **CREATE FORM**, **CREATE FROM**, **CREATE MENU**, CREATE PROJECT, **CREATE QUERY**, **CREATE SCREEN**, **CREATE SCREEN - Quick Screen**, **CREATE SQL VIEW**, **CREATE TABLE - SQL**, CREATE VIEW, **DEACTIVATE MENU**, **DEACTIVATE POPUP**, **DEACTIVATE WINDOW**, DEBUG, **DEBUGOUT**, **DECLARE**, **DECLARE - DLL**, **DEFINE BAR**, **DEFINE CLASS**, **DEFINE MENU**, **DEFINE PAD**, **DEFINE POPUP**, **DEFINE WINDOW**, **DELETE**, **DELETE - SQL**, **DELETE CONNECTION**, **DELETE DATABASE**, **DELETE FILE**, **DELETE TAG**, **DELETE TRIGGER**, **DELETE VIEW**, **DIMENSION**, DIR or DIRECTORY, **DISPLAY**, DISPLAY CONNECTIONS, DISPLAY DATABASE, DISPLAY DLLS, DISPLAY FILES, **DISPLAY MEMORY**, DISPLAY OBJECTS, DISPLAY PROCEDURES, **DISPLAY STATUS**, **DISPLAY STRUCTURE**, DISPLAY TABLES, DISPLAY VIEWS, **DO**, **DO CASE ... ENDCASE**, DO FORM, **DO WHILE ... ENDDO**, **DOEVENTS**, **DROP TABLE**, **DROP VIEW**, **EDIT**, **EJECT**, EJECT PAGE, **END TRANSACTION**, **ERASE**, **ERROR**, **EXIT**, **EXPORT**, **EXTERNAL**, **FIND**, **FLUSH**, **FOR ... ENDFOR**, **FOR EACH ... ENDFOR**, **FREE TABLE**, **FUNCTION**, **GATHER**, **GETEXPR**, **GO**, **GOTO**, **HIDE MENU**, **HIDE POPUP**, **HIDE WINDOW**, **IF ... ENDIF**, **IMPORT**, **INDEX**, **INPUT**, **INSERT**, **INSERT - SQL**, **JOIN**, **KEYBOARD**, **LIST**, **LIST CONNECTIONS**, **LIST DATABASE**, LIST DLLS, LIST OBJECTS, **LIST PROCEDURES**, LIST TABLES, LIST VIEWS, **LOCAL**, **LOCATE**, **LOOP**, **LPARAMETERS**, **MD**, **MENU**, **MENU TO**, **MKDIR**, MODIFY CLASS, MODIFY COMMAND, **MODIFY CONNECTION**, MODIFY DATABASE, MODIFY FILE, MODIFY FORM, MODIFY LABEL, **MODIFY MEMO**, MODIFY MENU, MODIFY PROCEDURE, MODIFY PROJECT, MODIFY QUERY, MODIFY REPORT, **MODIFY STRUCTURE**, MODIFY VIEW, **MODIFY WINDOW**, **MOUSE**, **MOVE POPUP**, **MOVE WINDOW**, **NOTE**, **ON BAR**, **ON ERROR**, **ON ESCAPE**, **ON EXIT BAR**, **ON EXIT MENU**, **ON EXIT PAD**, **ON EXIT POPUP**, **ON KEY**, **ON KEY =**, **ON KEY LABEL**, **ON PAD**, **ON PAGE**, **ON READERROR**, **ON SELECTION BAR**, **ON SELECTION MENU**, **ON SELECTION PAD**, **ON SELECTION POPUP**, **ON SHUTDOWN**, **OPEN DATABASE**, **PACK**, **PACK DATABASE**, **PARAMETERS**, **POP KEY**, **POP MENU**, **POP POPUP**, **PRINTJOB ... ENDPRINTJOB**, **PRIVATE**, **PROCEDURE**, **PUBLIC**, **PUSH KEY**, **PUSH MENU**, **PUSH POPUP**, QUIT, **RD**, **READ**, READ EVENTS, READ MENU, **RECALL**, **REINDEX**, **RELEASE**, **RELEASE BAR**, RELEASE CLASSLIB, RELEASE LIBRARY, **RELEASE MENUS**, **RELEASE PAD**, **RELEASE POPUPS**, **RELEASE PROCEDURE**, **RELEASE WINDOWS**, **REMOVE TABLE**, **RENAME**, RENAME CLASS, **RENAME CONNECTION**, **RENAME TABLE**, **RENAME VIEW**, **REPLACE**, **REPLACE FROM ARRAY**, **REPORT FORM**, **RESTORE FROM**, **RESTORE SCREEN**, **RESTORE WINDOW**, **RESUME**, **RETRY**, **RETURN**, **RMDIR**, **ROLLBACK**, **RUN**, **SAVE SCREEN**, **SAVE TO**, **SAVE WINDOWS**, **SCAN ... ENDSCAN**, **SCATTER**, **SCROLL**, **SEEK**, **SELECT**, **SELECT - SQL**, **SET ASSERTS**, **SET CENTURY**, **SET CURRENCY**, **SET DATABASE**, **SET DATE**, **SET DEBUG**, **SET DEBUGOUT**, **SET DECIMALS**, **SET DEFAULT**, **SET DELETED**, **SET ECHO**, **SET ESCAPE**, **SET EXACT**, **SET FDOW**, **SET FILTER**, **SET FIXED**, **SET FWEEK**, **SET HEADINGS**, **SET HOURS**, **SET INDEX**, **SET KEY**, **SET LIBRARY**, **SET MARK OF**, **SET MARK TO**, **SET MEMOWIDTH**, **SET MESSAGE**, **SET NEAR**, **SET NULLDISPLAY**, **SET ORDER**, **SET PATH**, **SET POINT**, **SET PROCEDURE**, **SET RELATION**, **SET RELATION OFF**, **SET SAFETY**, **SET SECONDS**, **SET SEPARATOR**, **SET SKIP OF**, **SET SYSMENU**, **SET TALK**, **SET TEXTMERGE**, **SET TEXTMERGE DELIMITERS**, **SET UNIQUE**, **SHOW GET**, **SHOW GETS**, **SHOW MENU**, **SHOW OBJECT**, **SHOW POPUP**, **SHOW WINDOW**, **SIZE POPUP**, **SIZE WINDOW**, **SKIP**, **SORT**, **STORE**, **SUM**, **SUSPEND**, **TEXT ... ENDTEXT**, **TOTAL**, **TRY...CATCH...FINALLY**, **TYPE**, **UNLOCK**, UPDATE, **UPDATE - SQL**, **USE**, **VALIDATE DATABASE**, **WAIT**, **WITH ... ENDWITH**, **ZAP**, **ZOOM WINDOW** -### Accepted and ignored (76) +### Accepted and ignored (75) -SET ALTERNATE, **SET ANSI**, **SET AUTOINCERROR**, **SET AUTOSAVE**, **SET BELL**, **SET BLOCKSIZE**, **SET BORDER**, **SET BROWSEIME**, **SET BRSTATUS**, **SET CARRY**, **SET CLASSLIB**, **SET CLEAR**, **SET CLOCK**, **SET COLLATE**, **SET COLOR OF**, **SET COLOR OF SCHEME**, SET COLOR SET, **SET COLOR TO**, **SET COMPATIBLE**, **SET CONFIRM**, **SET CONSOLE**, **SET COVERAGE**, **SET CPCOMPILE**, **SET CPDIALOG**, **SET CURSOR**, **SET DATASESSION**, **SET DELIMITERS**, **SET DEVELOPMENT**, **SET DEVICE**, **SET DISPLAY**, **SET DOHISTORY**, **SET ENGINEBEHAVIOR**, **SET EVENTLIST**, **SET EVENTTRACKING**, **SET EXCLUSIVE**, **SET FIELDS**, **SET FORMAT**, **SET FULLPATH**, **SET FUNCTION**, **SET HELP**, **SET INTENSITY**, **SET KEYCOMP**, **SET LOCK**, **SET LOGERRORS**, **SET MACKEY**, **SET MARGIN**, **SET MULTILOCKS**, **SET NOCPTRANS**, **SET NOTIFY**, **SET NULL**, **SET ODOMETER**, **SET OLEOBJECT**, **SET OPTIMIZE**, **SET PALETTE**, **SET PDSETUP**, **SET PRINTER**, **SET PROCEDURE**, **SET READBORDER**, **SET REFRESH**, **SET REPROCESS**, **SET RESOURCE**, **SET SKIP**, **SET SPACE**, **SET STATUS**, **SET STATUS BAR**, **SET STEP**, **SET STRICTDATE**, **SET SYSFORMATS**, **SET TABLEVALIDATE**, **SET TOPIC**, **SET TOPIC ID**, **SET TRBETWEEN**, **SET TYPEAHEAD**, **SET UDFPARMS**, **SET VIEW**, **SET WINDOW OF MEMO** +SET ALTERNATE, **SET ANSI**, **SET AUTOINCERROR**, **SET AUTOSAVE**, **SET BELL**, **SET BLOCKSIZE**, **SET BORDER**, **SET BROWSEIME**, **SET BRSTATUS**, **SET CARRY**, **SET CLASSLIB**, **SET CLEAR**, **SET CLOCK**, **SET COLLATE**, **SET COLOR OF**, **SET COLOR OF SCHEME**, SET COLOR SET, **SET COLOR TO**, **SET COMPATIBLE**, **SET CONFIRM**, **SET CONSOLE**, **SET COVERAGE**, **SET CPCOMPILE**, **SET CPDIALOG**, **SET CURSOR**, **SET DATASESSION**, **SET DELIMITERS**, **SET DEVELOPMENT**, **SET DEVICE**, **SET DISPLAY**, **SET DOHISTORY**, **SET ENGINEBEHAVIOR**, **SET EVENTLIST**, **SET EVENTTRACKING**, **SET EXCLUSIVE**, **SET FIELDS**, **SET FORMAT**, **SET FULLPATH**, **SET FUNCTION**, **SET HELP**, **SET INTENSITY**, **SET KEYCOMP**, **SET LOCK**, **SET LOGERRORS**, **SET MACKEY**, **SET MARGIN**, **SET MULTILOCKS**, **SET NOCPTRANS**, **SET NOTIFY**, **SET NULL**, **SET ODOMETER**, **SET OLEOBJECT**, **SET OPTIMIZE**, **SET PALETTE**, **SET PDSETUP**, **SET PRINTER**, **SET READBORDER**, **SET REFRESH**, **SET REPROCESS**, **SET RESOURCE**, **SET SKIP**, **SET SPACE**, **SET STATUS**, **SET STATUS BAR**, **SET STEP**, **SET STRICTDATE**, **SET SYSFORMATS**, **SET TABLEVALIDATE**, **SET TOPIC**, **SET TOPIC ID**, **SET TRBETWEEN**, **SET TYPEAHEAD**, **SET UDFPARMS**, **SET VIEW**, **SET WINDOW OF MEMO** ### Answered, not missing (4) diff --git a/src/renderer/src/runtime/session.ts b/src/renderer/src/runtime/session.ts index d810041..240b04f 100644 --- a/src/renderer/src/runtime/session.ts +++ b/src/renderer/src/runtime/session.ts @@ -1339,7 +1339,13 @@ export const useSessionStore = create((set, get) => { case 'LoadProgram': return (async () => { const compiled = await source.getProgram(baseName(request.name)); - if (!compiled) throw new HostError(1, `File '${request.name}' does not exist.`); + if (!compiled) { + // the product's words: the name in lower case, with a program's extension when it + // came without one - `File 'hello.prg' does not exist.` + const file = request.name.split(/[\\/]/).pop() ?? request.name; + const shown = file === file.toUpperCase() ? file.toLowerCase() : file; + throw new HostError(1, `File '${shown.includes('.') ? shown : `${shown}.prg`}' does not exist.`); + } const cached = modules.get(compiled.name.toLowerCase()); if (cached !== undefined) return cached; const id = vm.loadModule(compiled.bytes); diff --git a/tests/reference/coverage.test.ts b/tests/reference/coverage.test.ts index 17ada15..71cc8b9 100644 --- a/tests/reference/coverage.test.ts +++ b/tests/reference/coverage.test.ts @@ -101,6 +101,7 @@ const SET_ELSEWHERE: Record = { MESSAGE: { file: 'crates/foxvm/src/menu.rs', find: 'SET MESSAGE' }, TEXTMERGE: { file: 'crates/foxvm/src/parser.rs', find: 'TEXTMERGE' }, DATABASE: { file: 'crates/foxvm/src/vm.rs', find: 'SET DATABASE' }, + PROCEDURE: { file: 'crates/foxvm/src/vm.rs', find: 'setting == "PROCEDURE"' }, }; function manifest(): Element[] {