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
3 changes: 2 additions & 1 deletion crates/foxvm/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ pub trait BuiltinCtx {
fn object_members(&mut self, obj: crate::value::Handle) -> Option<Vec<crate::host::MemberInfo>>;
/// The object whose method is running, for DODEFAULT().
fn running_object(&self) -> Option<u32>;
/// What that method is called.
/// What that method is compiled as: `CLASS.EVENT` for a class of a program, `PATH.EVENT` for
/// an object's own, with an ancestor's copy of an overridden method marked `EVENT#n`.
fn running_method(&self) -> String;
/// COMARRAY(): how an array is passed to that COM object, as it was last set.
fn com_setting(&self, obj: u32) -> i64;
Expand Down
8 changes: 5 additions & 3 deletions crates/foxvm/src/builtins/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,14 @@ fn f_dodefault(c: &mut dyn BuiltinCtx, a: Vec<Value>) -> Result<BuiltinResult, R
let Some(obj) = c.running_object() else {
return Err(RtError::new(1928, "DODEFAULT() can only be used inside a method"));
};
let method = c.running_method();
let from = c.running_method();
let method = from.rsplit('.').next().unwrap_or(&from).to_string();
if method.is_empty() {
return Err(RtError::new(1928, "DODEFAULT() can only be used inside a method"));
}
let args = a.iter().map(JsonValue::from_value).collect();
Ok(BuiltinResult::Suspend(HostRequest::CallParentMethod { obj, method, args }))
// `DODEFAULT(@m.nError, ...)` hands the parent the same variables
let args = a.iter().map(JsonValue::from_arg).collect();
Ok(BuiltinResult::Suspend(HostRequest::CallParentMethod { obj, method, args, from }))
}

/// `COMPROP(oObject, cProperty [, eValue])`: a setting on how the runtime talks to that COM
Expand Down
2 changes: 0 additions & 2 deletions crates/foxvm/src/bytecode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,8 +492,6 @@ pub enum Instr {
to: bool,
},
NoDefault,
/// [args...] -> [v]
DoDefault(u8),

// ---- exceptions
/// Installs a handler; either target may be `NO_TARGET`. On an error the VM unwinds to the
Expand Down
32 changes: 24 additions & 8 deletions crates/foxvm/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2209,9 +2209,12 @@ impl ModuleCompiler {
StmtKind::NoDefault => {
fb.emit(Instr::NoDefault);
}
// `DODEFAULT()` on a line of its own runs the parent's code as the function form
// does, and the answer is dropped
StmtKind::DoDefault(args) => {
let argc = self.args(fb, args, false, false);
fb.emit(Instr::DoDefault(argc));
let (id, _) = builtins::lookup("DODEFAULT").expect("DODEFAULT is a builtin");
let argc = self.args(fb, args, true, false);
fb.emit(Instr::CallBuiltin { id, argc });
fb.emit(Instr::Pop);
}
StmtKind::Text { target, additive, textmerge, noshow, raw } => {
Expand Down Expand Up @@ -3693,6 +3696,22 @@ impl ModuleCompiler {
continue;
}
ExprKind::Var(_) => self.warning(a.expr.span, "Argument is passed by value here"),
// `@m.uArg1`: the `m.` says a memory variable, which is what `@` passes, so
// it is the variable by reference - unless a local is really called M
ExprKind::Member { obj, name }
if a.by_ref
&& matches!(&obj.kind, ExprKind::Var(base) if base.upper == "M")
&& fb.local("M").is_none() =>
{
if allow_ref {
let v = self.var_target(fb, name);
fb.emit(Instr::Ref(v));
continue;
}
// where a plain `@name` is passed by value - a method of an object - so
// is this, and with the same warning
self.warning(a.expr.span, "Argument is passed by value here");
}
_ if a.by_ref => self.error(a.expr.span, "Only a variable can be passed by reference"),
_ => {}
}
Expand Down Expand Up @@ -3876,9 +3895,11 @@ impl ModuleCompiler {
let n = self.subscripts(fb, args, e.span);
fb.emit(Instr::LoadIndex(n));
}
// `@var` reaches a method as the variable itself: an object's method of FoxPro code
// writes to it as a procedure does, which is how CodeMine hands back a second answer
ExprKind::MethodCall { obj, name, args } => {
self.expr(fb, obj);
let argc = self.args(fb, args, false, false);
let argc = self.args(fb, args, true, false);
let m = self.member(name);
fb.emit(Instr::CallMethod { name: m, argc });
}
Expand Down Expand Up @@ -3996,11 +4017,6 @@ impl ModuleCompiler {
fb.emit(Instr::CallBuiltin { id, argc });
return;
}
if name.upper == "DODEFAULT" {
let argc = self.args(fb, args, false, false);
fb.emit(Instr::DoDefault(argc));
return;
}
let argc = self.args(fb, args, true, false);
let n = self.name(&name.upper);
fb.emit(Instr::IndexOrCall { name: n, argc });
Expand Down
62 changes: 61 additions & 1 deletion crates/foxvm/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,14 @@ pub enum HostRequest {
/// came from has no such method.
CallParentMethod {
obj: u32,
/// The method being run, which is the one to look for above it.
/// The method being run, which is the one to look for above it: its event, with the
/// `#n` of an ancestor's copy when that is what is running.
method: String,
args: Vec<JsonValue>,
/// The whole name the running code was compiled under - `CLASS.EVENT` for a class of a
/// program - which says whose code it is, and so where above it to start looking.
#[serde(default)]
from: String,
},
/// `BUILD APP | EXE | DLL | MTDLL | PROJECT`: a project turned into the file that ships,
/// or the project itself built out of the files it names. Resume with Null; a build that
Expand Down Expand Up @@ -861,6 +866,16 @@ pub enum JsonValue {
#[serde(rename = "$fn")]
function: u32,
},
/// A variable passed by reference to a method - `o.GetClassName(cAlias, @m.cLibrary)`. It
/// travels as a number naming the variable's cell, with the value beside it for a host that
/// only wants to read it; when the host runs the method's code the number comes back, and
/// the parameter is that same cell, so what the method writes the caller sees.
Ref {
#[serde(rename = "$ref")]
cell: u32,
#[serde(rename = "$val")]
value: Box<JsonValue>,
},
Date {
#[serde(rename = "$date")]
date: String,
Expand Down Expand Up @@ -922,8 +937,19 @@ impl JsonValue {
}
}

/// A call's argument: a variable passed by reference crosses as a `Ref`, everything else as
/// its value.
pub fn from_arg(v: &Value) -> JsonValue {
match v {
Value::Ref(cell) => JsonValue::Ref { cell: ref_cells::hold(cell), value: Box::new(JsonValue::from_value(v)) },
other => JsonValue::from_value(other),
}
}

pub fn to_value(&self) -> Value {
match self {
// the variable itself, when it is still there; otherwise the value it crossed with
JsonValue::Ref { cell, value } => ref_cells::find(*cell).map_or_else(|| value.to_value(), Value::Ref),
JsonValue::Null => Value::Null,
JsonValue::Bool(b) => Value::Logical(*b),
JsonValue::Num(n) => Value::number(*n),
Expand Down Expand Up @@ -1080,3 +1106,37 @@ pub struct BrowseRow {
pub deleted: bool,
pub values: Vec<JsonValue>,
}

/// The variables passed by reference that are out in the host, by the number they crossed as.
///
/// Held weakly: the variable belongs to the routine that declared it, and a number the host keeps
/// after that routine has returned finds nothing, rather than keeping the variable alive.
mod ref_cells {
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::{Rc, Weak};

use crate::value::Value;

thread_local! {
static CELLS: RefCell<(u32, HashMap<u32, Weak<RefCell<Value>>>)> = RefCell::new((0, HashMap::new()));
}

pub fn hold(cell: &Rc<RefCell<Value>>) -> u32 {
CELLS.with(|c| {
let mut c = c.borrow_mut();
// numbers for variables that have gone are let go of now and then
if c.1.len() > 4096 {
c.1.retain(|_, w| w.strong_count() > 0);
}
c.0 = c.0.wrapping_add(1);
let id = c.0;
c.1.insert(id, Rc::downgrade(cell));
id
})
}

pub fn find(id: u32) -> Option<Rc<RefCell<Value>>> {
CELLS.with(|c| c.borrow().1.get(&id).and_then(Weak::upgrade))
}
}
2 changes: 1 addition & 1 deletion crates/foxvm/src/mock_host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,7 @@ impl MockHost {
HostRequest::GetObject { .. } => Value::Null,
HostRequest::RunLine { .. } => Value::Null,
HostRequest::NewDocument { .. } => Value::Null,
HostRequest::CallParentMethod { .. } => Value::Logical(false),
HostRequest::CallParentMethod { .. } => Value::Logical(true),
// no data source to reach, which is what a connection that cannot be made answers
HostRequest::Sql { .. } => Value::number(-1.0),
HostRequest::CloseMemo { .. } => Value::Null,
Expand Down
65 changes: 56 additions & 9 deletions crates/foxvm/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,9 @@ pub struct Vm {
/// 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<u32>,
/// The last error raised anywhere in the session, for ERROR() and MESSAGE() asked from a
/// fiber that has not raised one of its own - a class's Error method, run by the host.
last_error: Option<RtError>,
/// Objects whose `Error` method is running, so an error inside it is not handed back to it.
in_class_error: HashSet<u32>,
/// The work area the last USE opened, so the index beside it can be read next.
Expand Down Expand Up @@ -1565,6 +1568,7 @@ impl Vm {
library_funcs: HashMap::new(),
procedure_files: Vec::new(),
procedure_only: HashSet::new(),
last_error: None,
last_opened: None,
index_build: None,
idx_open: None,
Expand Down Expand Up @@ -2495,6 +2499,7 @@ impl Vm {
/// Gives the error to the nearest handler that wants it.
fn handle(&mut self, host: &mut dyn Host, fb: &mut Fiber, err: RtError, floor: usize) -> Option<Step> {
fb.last_error = Some(err.clone());
self.last_error = Some(err.clone());
// An error raised while a HAVING predicate was running leaves the query saying that no
// record is current. A CATCH that swallows it must not leave every later field read
// complaining, so the clause is no longer running the moment the error is handed on.
Expand All @@ -2515,6 +2520,33 @@ impl Vm {
fb.query_unwind = fb.query.take();
}
}
// Measured in Visual FoxPro 9, an error raised inside an object's Error method goes one of
// two ways, and a TRY inside the method is nearer than either:
// - the program called the method (or DODEFAULT() reached an ancestor's from there): the
// error goes neither to Error again nor to ON ERROR, and the method carries on at its
// next statement;
// - the runtime called it, because a method of the object raised an error: the error is
// not skipped and goes to the default handler, the dialog, without ON ERROR being asked.
// `in_class_error` holds the object while the runtime is running its Error method.
let mut in_runtime_error_method = false;
if let Some((ix, obj)) = self.running_error_method(fb, floor)
&& !fb.handlers.last().is_some_and(|h| h.frame >= ix)
{
if self.in_class_error.contains(&obj) {
in_runtime_error_method = true;
} else {
let at = fb.frames.len() - 1;
let (resume_pc, stmt_sp) = {
let fr = &fb.frames[at];
let code = &self.proto(fr.module, fr.func).code;
let next = (fr.pc..code.len()).find(|&i| matches!(code[i], Instr::Stmt(_))).unwrap_or(code.len().saturating_sub(2));
(next, fr.stmt_sp)
};
fb.stack.truncate(stmt_sp);
fb.frames[at].pc = resume_pc;
return None;
}
}
if let Some(step) = self.class_error_method(host, fb, &err, floor) {
return Some(step);
}
Expand Down Expand Up @@ -2550,6 +2582,7 @@ impl Vm {
}
if floor == 0
&& !fb.in_error_handler
&& !in_runtime_error_method
&& !fb.frames.is_empty()
&& let Some(text) = self.on_error.clone()
{
Expand All @@ -2558,6 +2591,22 @@ impl Vm {
Some(Step::Error(err))
}

/// The innermost frame, at or above `floor`, that is running some object's Error method -
/// its own, or an ancestor's copy `ERROR#n` - when the error was raised in that very frame,
/// with the object whose method it is.
fn running_error_method(&self, fb: &Fiber, floor: usize) -> Option<(usize, u32)> {
let at = fb.frames.len().checked_sub(1)?;
if at < floor {
return None;
}
let fr = &fb.frames[at];
let this = fr.this?;
let name = &self.proto(fr.module, fr.func).display_name;
let event = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
let base = event.split('#').next().unwrap_or(&event);
(base == "ERROR").then_some((at, this.0))
}

/// VFP hands an error raised inside a method to the object's own `Error` method, unless a
/// TRY inside that same method catches it first. The method runs through the host like any
/// other, and the failing method then carries on at its next statement.
Expand Down Expand Up @@ -3843,7 +3892,7 @@ impl Vm {
return Ok(Flow::Suspend(HostRequest::CallMethod {
obj: h.0,
name: name.clone(),
args: args.iter().map(JsonValue::from_value).collect(),
args: args.iter().map(JsonValue::from_arg).collect(),
}));
}
Instr::PushWith => {
Expand Down Expand Up @@ -4066,10 +4115,6 @@ impl Vm {
fb.frames.last_mut().expect("frame").nodefault = true;
fb.nodefault = true;
}
Instr::DoDefault(argc) => {
pop_n(fb, *argc as usize)?;
fb.stack.push(Value::Logical(true));
}
Instr::TryPush { catch, finally } => {
let frame = fb.frames.len() - 1;
let env = env_index(fb, frame);
Expand Down Expand Up @@ -10085,11 +10130,11 @@ impl BuiltinCtx for Ctx<'_> {
}

fn running_method(&self) -> String {
// a method is compiled under "OBJECT.EVENT"; DODEFAULT() is about the event
// the whole of what it was compiled as: DODEFAULT() needs the event, and which class's
// code is running, which is what says where to look above it
let env = env_index(self.fiber, self.fiber.frames.len() - 1);
let f = &self.fiber.frames[env];
let name = self.vm.proto(f.module, f.func).display_name.clone();
name.rsplit('.').next().unwrap_or(&name).to_string()
self.vm.proto(f.module, f.func).display_name.clone()
}

fn com_setting(&self, obj: u32) -> i64 {
Expand Down Expand Up @@ -10174,7 +10219,9 @@ impl BuiltinCtx for Ctx<'_> {
self.vm.load_name(self.fiber, upper_name)
}
fn last_error(&self) -> Option<RtError> {
self.fiber.last_error.clone()
// ERROR() and MESSAGE() are the session's last error, not this fiber's: a class's Error
// method is run on a fiber of its own, and asks them about the error that sent it there
self.fiber.last_error.clone().or_else(|| self.vm.last_error.clone())
}
fn on_error_command(&self) -> Option<String> {
self.vm.on_error.clone()
Expand Down
2 changes: 2 additions & 0 deletions crates/foxvm/tests/programs/byref_mdot.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
42 after
20 2
23 changes: 23 additions & 0 deletions crates/foxvm/tests/programs/byref_mdot.prg
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
* COVERS: @
*
* `@m.name` passes the memory variable by reference, as `@name` does: CodeMine's own support
* library writes every by-reference argument that way. Measured in Visual FoxPro 9.
LOCAL lnValue, lcText
lnValue = 1
lcText = "before"
= setboth(@m.lnValue, @m.lcText)
? lnValue, lcText

* and in a call written as a statement's value, through several of them at once
lnValue = 1
? countup(@m.lnValue), lnValue

FUNCTION setboth(tnValue, tcText)
tnValue = 42
tcText = "after"
ENDFUNC

FUNCTION countup(tnValue)
tnValue = tnValue + 1
RETURN tnValue * 10
ENDFUNC
2 changes: 1 addition & 1 deletion docs/language-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Every element of the Visual FoxPro language, and what happens when a program use
| directive | 7 | 0 | 0 | 0 | 0 | 7 |
| systemvar | 49 | 0 | 0 | 0 | 0 | 49 |

1534 element(s) are exercised by a test.
1535 element(s) are exercised by a test.

## command

Expand Down
9 changes: 8 additions & 1 deletion src/renderer/src/runtime/classLibraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export class ClassLibraries {
constructor(
private readonly readTable: (path: string) => Promise<DbfTableData>,
private readonly search: readonly string[] = [],
/** Told when a library a class stands on cannot be found, so an object is not silently short of its parent's code. */
private readonly onMissing: (library: string, from: string) => void = () => {},
) {}

/**
Expand Down Expand Up @@ -92,7 +94,12 @@ export class ClassLibraries {
const table = await this.readTable(path);
// a class in the file may stand on one in another file, and an object of it needs the whole
// lineage; the importer follows those references itself, given somewhere to look
const { libraries } = await loadClassLibraries(table, dirname(path), this.readTable, [dirname(path), ...this.search]);
const { libraries, missing } = await loadClassLibraries(table, dirname(path), this.readTable, [dirname(path), ...this.search]);
// a class standing on another in the same file names its own library, which is not missing
const own = basename(path).toLowerCase();
for (const library of missing) {
if (basename(library.replace(/\\/g, '/')).toLowerCase() !== own) this.onMissing(library, path);
}
const doc = importClassLibraryDocument(table, name, libraries).doc;
this.read.set(path.toLowerCase(), doc);
return { path, alias: alias || name, doc };
Expand Down
Loading
Loading