Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions crates/foxvm/src/builtins/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,14 @@ fn f_pad(c: &mut dyn BuiltinCtx, _a: Vec<Value>) -> Result<BuiltinResult, RtErro
}

/// POPUP(): the name of the popup that is up, or the one last chosen from.
fn f_popup(c: &mut dyn BuiltinCtx, _a: Vec<Value>) -> Result<BuiltinResult, RtError> {
/// POPUP([cMenuName]). With no argument, the popup that is active. With one, whether a popup of
/// that name is defined - measured: a logical, the name compared without regard to case and
/// without trimming, and a number there error 11.
fn f_popup(c: &mut dyn BuiltinCtx, a: Vec<Value>) -> Result<BuiltinResult, RtError> {
if let Some(named) = a.first() {
let Value::Str(name) = named.deref() else { return Err(RtError::function_arg_invalid()) };
return value(Value::Logical(c.menus().popup(&name).is_some()));
}
let menus = c.menus();
let name = if menus.active_popup.is_empty() { &menus.last_popup } else { &menus.active_popup };
value(Value::str(name.to_ascii_uppercase()))
Expand Down Expand Up @@ -218,7 +225,7 @@ pub fn specs() -> Vec<BuiltinSpec> {
spec("MRKBAR", 2, 2, f_mrkbar),
spec("MRKPAD", 2, 2, f_mrkpad),
spec("PAD", 0, 0, f_pad),
spec("POPUP", 0, 0, f_popup),
spec("POPUP", 0, 1, f_popup),
spec("PRMBAR", 2, 2, f_prmbar),
spec("PRMPAD", 2, 2, f_prmpad),
spec("PROMPT", 0, 0, f_prompt),
Expand Down
17 changes: 14 additions & 3 deletions crates/foxvm/src/builtins/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,22 @@ fn f_forcepath(_c: &mut dyn BuiltinCtx, a: Vec<Value>) -> Result<BuiltinResult,
// environment
// ------------------------------------------------------------------------------------------

fn f_version(_c: &mut dyn BuiltinCtx, a: Vec<Value>) -> Result<BuiltinResult, RtError> {
/// VERSION([nExpression]). Measured in Visual FoxPro 9: 2 is a number, the edition - 2 in the
/// development environment and 0 in the runtime - and 5 is the number 900, which is what a
/// program compares against before it uses anything new in version 9. 3 is the language, "00"
/// for English, and 0 or anything past 5 is error 11. The product's name in 1 and the plain
/// form is this runtime's own.
fn f_version(c: &mut dyn BuiltinCtx, a: Vec<Value>) -> Result<BuiltinResult, RtError> {
if a.is_empty() {
return ok(Value::str(format!("FoxDev Studio Runtime {}", crate::VERSION)));
}
match opt_int(&a, 0, 1)? {
1 => ok(Value::str(format!("FoxDev Studio Runtime {}", crate::VERSION))),
2 => ok(Value::number(if c.settings().runtime_only { 0.0 } else { 2.0 })),
3 => ok(Value::str("00")),
4 => ok(Value::str(crate::VERSION)),
5 => ok(Value::number(0.0)),
_ => ok(Value::str(format!("FoxDev Studio Runtime {}", crate::VERSION))),
5 => ok(Value::number(900.0)),
_ => Err(RtError::function_arg_invalid()),
}
}

Expand Down
28 changes: 22 additions & 6 deletions crates/foxvm/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ pub fn parse_method(src: &str) -> ParseOutput {
pub fn parse_method_with(src: &str, headers: &std::collections::HashMap<String, String>, include: &str) -> ParseOutput {
let mut p = Parser::new(src, Mode::Method);
p.headers = headers.clone();
if let Some(text) = p.headers.get(&header_stem(include)).cloned() {
p.read_header(&text);
let stem = header_stem(include);
if let Some(text) = p.headers.get(&stem).cloned() {
p.read_header(&stem, &text);
}
let program = p.program();
ParseOutput { program, diagnostics: p.diags }
Expand Down Expand Up @@ -442,6 +443,10 @@ struct Parser {
src: Vec<char>,
/// The header files this program may include, by name without folder or extension.
headers: std::collections::HashMap<String, String>,
/// The headers being read right now, innermost last. A header that includes itself, or one
/// that includes it back, is read once: a second `#INCLUDE` of a header already open brings
/// nothing the first did not, and following it would never end.
headers_open: Vec<String>,
toks: Vec<Token>,
/// Parallel to `toks`: false for tokens produced by `#DEFINE` expansion (never re-expanded).
expandable: Vec<bool>,
Expand All @@ -466,6 +471,7 @@ impl Parser {
Parser {
src: src.chars().collect(),
headers: std::collections::HashMap::new(),
headers_open: Vec::new(),
toks: lexed.tokens,
expandable: vec![true; n],
pos: 0,
Expand Down Expand Up @@ -6992,7 +6998,7 @@ impl Parser {
match self.headers.get(&stem) {
Some(text) => {
let text = text.clone();
self.read_header(&text);
self.read_header(&stem, &text);
}
None => {
let span = hash.span.to(name_tok.span);
Expand All @@ -7016,7 +7022,16 @@ impl Parser {

/// Reads a header file's constants into this parse: its `#DEFINE` lines, and the ones of
/// any header it includes in turn.
fn read_header(&mut self, text: &str) {
fn read_header(&mut self, stem: &str, text: &str) {
if self.headers_open.iter().any(|open| open == stem) {
return;
}
self.headers_open.push(stem.to_string());
self.read_header_text(text);
self.headers_open.pop();
}

fn read_header_text(&mut self, text: &str) {
let tokens = crate::lexer::lex(text).tokens;
let mut at = 0usize;
while at < tokens.len() {
Expand Down Expand Up @@ -7056,8 +7071,9 @@ impl Parser {
_ => String::new(),
})
.collect();
if let Some(text) = self.headers.get(&header_stem(&file)).cloned() {
self.read_header(&text);
let stem = header_stem(&file);
if let Some(text) = self.headers.get(&stem).cloned() {
self.read_header(&stem, &text);
}
}
_ => {}
Expand Down
4 changes: 4 additions & 0 deletions crates/foxvm/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ pub struct Settings {
pub talk: bool,
pub safety: bool,
pub escape: bool,
/// Running as a built application rather than in the IDE: what `VERSION(2)` answers, 0 for
/// the runtime and 2 for the development edition. The IDE's player turns it on.
pub runtime_only: bool,
/// SET DELETED ON hides deleted records from every movement command.
pub deleted: bool,
/// SET NEAR ON leaves the pointer at the nearest record when a SEEK misses, rather than at
Expand Down Expand Up @@ -535,6 +538,7 @@ impl Default for Settings {
talk: true,
safety: true,
escape: true,
runtime_only: false,
deleted: false,
near: false,
unique: false,
Expand Down
1 change: 1 addition & 0 deletions crates/foxvm/src/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@ impl FoxVm {
"TALK" => s.talk = v.truthy().unwrap_or(true),
"SAFETY" => s.safety = v.truthy().unwrap_or(true),
"ESCAPE" => s.escape = v.truthy().unwrap_or(true),
"RUNTIME" => s.runtime_only = v.truthy().unwrap_or(false),
"DECIMALS" => s.decimals = v.as_number().unwrap_or(2.0).clamp(0.0, 18.0) as u8,
"DATE" => {
if let Some(f) = v.as_str().ok().and_then(|w| crate::value::DateFormat::parse(&w)) {
Expand Down
7 changes: 7 additions & 0 deletions crates/foxvm/tests/builtins_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ fn file_requests() {
fn environment_functions() {
assert_eq!(text("VERSION", vec![]), format!("FoxDev Studio Runtime {}", foxvm::VERSION));
assert_eq!(text("VERSION", vec![n(4.0)]), foxvm::VERSION);
// measured: the edition and the version number are numbers, a program compares both, and
// the edition is the development one unless the host says this is a built application
assert_eq!(num("VERSION", vec![n(2.0)]), 2.0);
assert_eq!(text("VERSION", vec![n(3.0)]), "00");
assert_eq!(num("VERSION", vec![n(5.0)]), 900.0);
assert_eq!(err("VERSION", vec![n(0.0)]).code, RtError::FUNCTION_ARG_INVALID);
assert_eq!(err("VERSION", vec![n(6.0)]).code, RtError::FUNCTION_ARG_INVALID);
// a FoxPro program that guards on the Windows version reads OS(3) and OS(4)
assert_eq!(text("OS", vec![]), "Windows 10.00");
// OS(2) is DBCS support, not the version - measured against the product empty on the code
Expand Down
9 changes: 9 additions & 0 deletions crates/foxvm/tests/directives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ fn an_included_header_brings_its_constants() {
let text = dump(&out.program).lines().filter(|l| !l.contains("(directive")).collect::<Vec<_>>().join(" ");
assert_eq!(text, "(= X (+ 80 25))");

// headers that include themselves, or each other, are read once each rather than forever
let mut looped = HashMap::new();
looped.insert("SELF".to_string(), "#INCLUDE self.h\n#DEFINE A 1\n".to_string());
looped.insert("PING".to_string(), "#INCLUDE pong.h\n#DEFINE B 2\n".to_string());
looped.insert("PONG".to_string(), "#INCLUDE ping.h\n#DEFINE C 3\n".to_string());
let out = parse_program_with("#INCLUDE self.h\n#INCLUDE ping.h\nx = A + B + C\n", &looped);
let text = dump(&out.program).lines().filter(|l| !l.contains("(directive")).collect::<Vec<_>>().join(" ");
assert_eq!(text, "(= X (+ (+ 1 2) 3))");

// and one that is not there is a warning, not an error
let out = parse_program("#INCLUDE nowhere.h\nx = 1\n");
assert!(out.diagnostics.iter().all(|d| !d.is_error()));
Expand Down
5 changes: 5 additions & 0 deletions crates/foxvm/tests/programs/ref_popup_named.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
before L .F.
after .T. .T.
padded .F.
released .F.
active C []
14 changes: 14 additions & 0 deletions crates/foxvm/tests/programs/ref_popup_named.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
* COVERS: POPUP
*
* POPUP(cMenuName) asks whether a popup of that name is defined, which is how an application
* that rebuilds its Window menu finds out whether it has one yet. Measured in Visual FoxPro 9.
? "before", TYPE([POPUP("fdvpop")]), POPUP("fdvpop")
DEFINE POPUP fdvpop
DEFINE BAR 1 OF fdvpop PROMPT "one"
? "after", POPUP("fdvpop"), POPUP("FDVPOP")
* the name is not trimmed
? "padded", POPUP(" fdvpop ")
RELEASE POPUPS fdvpop
? "released", POPUP("fdvpop")
* with no argument it is still the name of the popup that is active: none here
? "active", TYPE("POPUP()"), "[" + POPUP() + "]"
2 changes: 1 addition & 1 deletion docs/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ call fails with an explanation rather than "procedure not found".
| PCOUNT | 0 |
| PEMSTATUS | 3 |
| PI | 0 |
| POPUP | 0 |
| POPUP | 0-1 |
| PRIMARY | 0-2 |
| PRINTSTATUS | 0 |
| PRMBAR | 2 |
Expand Down
48 changes: 37 additions & 11 deletions src/renderer/src/runtime/projectSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
baseName,
formHeaderRefs,
formMethodSources,
headerStem,
requireBytes,
type CompiledForm,
type CompiledProgram,
Expand All @@ -26,7 +27,7 @@ import { refreshed } from '../vfp/refreshImport';
import { useDocumentsStore, type OpenDocument } from '../stores/documentsStore';
import { useProjectStore } from '../stores/projectStore';
import { readHeaderFiles } from './headerFiles';
import { compileForm, compileProgram, includedHeaders } from './vmBridge';
import { compileForm, compileProgram, includedHeaderNames } from './vmBridge';

/** Finds an open document whose file name matches, ignoring directory and extension. */
function openDocumentNamed(name: string, kind: OpenDocument['kind']): OpenDocument | undefined {
Expand All @@ -51,16 +52,36 @@ function projectItemPath(name: string, extensions: string[]): string | null {
}

/**
* The text of every header file a program includes, by name without folder or extension. A
* header the project does not have is left out: the compiler warns about it by name.
* The text of every header file a program includes, by name without folder or extension.
*
* A header the project lists is taken from there. The rest are looked for the way a form's are:
* beside the program, then in the default directory - the project folder - then among the
* Foundation Classes, following the headers they include in turn. A Visual FoxPro project
* rarely lists its `.h` files at all, so without the search every constant an application
* defines is an unknown name when the line that uses it runs. One found nowhere is left out,
* and the compiler warns about it by name.
*/
async function headerFiles(source: string): Promise<Record<string, string>> {
async function headerFiles(source: string, dir?: string): Promise<Record<string, string>> {
const out: Record<string, string> = {};
for (const stem of includedHeaders(source)) {
if (stem in out) continue;
const path = projectItemPath(stem, ['.h', '.prg']);
if (!path) continue;
out[stem] = await getApi().files.readText(path).catch(() => '');
const unlisted: string[] = [];
for (const reference of includedHeaderNames(source)) {
const stem = headerStem(reference);
if (stem === '' || stem in out) continue;
// the extension the reference wrote is the file it means: `#include "CodeMine.h"` in
// codemine.prg is the header beside it, and never the program itself, whose text would
// include the header again
const file = reference.trim().split(/[\\/]/).pop() ?? '';
const dot = file.lastIndexOf('.');
const extensions = dot > 0 ? [file.slice(dot).toLowerCase()] : ['.h', '.prg'];
const path = projectItemPath(stem, extensions);
if (path) out[stem] = await getApi().files.readText(path).catch(() => '');
else unlisted.push(reference);
}
if (unlisted.length > 0) {
const project = useProjectStore.getState();
const dirs = [dir ?? '', project.path ? dirname(project.path) : '', await bundledClassLibraryDir()].filter((d) => d !== '');
const found = await readHeaderFiles(unlisted, dirs.length > 0 ? dirs : ['']);
for (const [stem, text] of Object.entries(found)) if (!(stem in out)) out[stem] = text;
}
return out;
}
Expand Down Expand Up @@ -116,20 +137,25 @@ export function createProjectSource(): ProgramSource {
let source: string;
let key: string;
let display = baseName(name);
let dir: string | undefined;

if (open?.kind === 'program') {
source = open.text;
key = `open:${open.id}:${open.text.length}:${open.text.slice(0, 64)}`;
if (open.path) display = baseName(basename(open.path));
if (open.path) {
display = baseName(basename(open.path));
dir = dirname(open.path);
}
} else {
const path = projectItemPath(name, ['.prg']);
if (!path) return null;
source = await getApi().files.readText(path);
key = `file:${source.length}:${source.slice(0, 64)}`;
display = baseName(basename(path));
dir = dirname(path);
}

const headers = await headerFiles(source);
const headers = await headerFiles(source, dir);
const cached = programs.get(display, `${key}:${Object.keys(headers).join(',')}`);
if (cached) return cached;
const bytes = requireBytes(display, compileProgram(source, display, headers));
Expand Down
17 changes: 15 additions & 2 deletions src/renderer/src/runtime/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ export const runtimeUi = {
newDocument: async (kind: string, _path: string): Promise<void> => {
throw new Error(`CREATE ${kind.toUpperCase()} needs a designer, which the player has not`);
},
/**
* The development environment rather than a built application: `VERSION(2)` answers 2 here
* and 0 in the player, and programs branch on it to find their source.
*/
development: false,
};
const showDesktop = () => runtimeUi.showDesktop();

Expand Down Expand Up @@ -335,9 +340,17 @@ export const useSessionStore = create<SessionState>((set, get) => {
desktop.runLine = (text) => runSnippet(text, 'line').then(() => undefined);
desktop.setVariable = (name, value) => vm.setGlobal(name, value);
desktop.quitRequested = () => void get().cancel();
// SET LIBRARY TO reaches the process that can load a 32-bit .fll; there is none in a browser
desktop.libraries = getApi().library;
// SET LIBRARY TO reaches the process that can load a 32-bit .fll; there is none in a browser.
// A library named without a folder is the program's default directory's, which here is the
// project folder - `SET LIBRARY TO codemine.fll` means the one beside the application. Left
// alone, that process would resolve it against its own working directory instead.
const libraryHost = getApi().library;
desktop.libraries = libraryHost && {
...libraryHost,
load: (path: string) => libraryHost.load(isAbsolute(path) ? path : useProjectStore.getState().resolvePath(path)),
};
const vm = createVm(desktop);
vm.setSetting('RUNTIME', !runtimeUi.development);
// _SAMPLES names the samples directory, which is HOME(2). A program that opens
// _samples + "\Data\customer.dbf" reads it on its first line, so it is answered before
// anything runs rather than while it does.
Expand Down
1 change: 1 addition & 0 deletions src/renderer/src/shell/IdeLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function setShell(patch: Partial<ShellState>) {
registerIdeCommands({ toggle: (p) => setShell({ [p]: !shellState[p] }), get: (p) => shellState[p] });
// a form opened by any means (Run, a menu item, the Command Window) surfaces the desktop tab
runtimeUi.showDesktop = () => void useDocumentsStore.getState().openDesktop();
runtimeUi.development = true;
// a program that stops has to be seen stopping, however the debugger window was left
runtimeUi.showDebugger = () => setShell({ showDebugger: true });
// MODIFY COMMAND and BROWSE open a file in a tab: the same thing File > Open does
Expand Down
Loading