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[] {