Skip to content

Commit 671ecef

Browse files
committed
Implement the Needs rule forms and $ContextAliases
`Needs["ctx`" -> "alias`"]` records the alias in `$ContextAliases` and `Needs["ctx`" -> None]` records nothing; both leave `$ContextPath` exactly as they found it, undoing what the read file did to it. An alias stands for its context wherever the context's own name could appear: in a symbol, and in a `Names` or `Contexts` pattern. The alias is registered as soon as the rule has been read — so a call that goes on to fail on its second argument keeps it — and taken back out, restoring any previous target, when the load produces no context. `$ContextAliases::cxinuse` and `::cxconflict` are re-checked on every write to the mapping, as wolframscript does. Fixes found on the way there: - `$Path` was ignored: `Needs`, `Get` and `FindFile` searched a hardcoded list, so `AppendTo[$Path, dir]` had no effect. - `$ContextPath` was dual-sourced — assignments landed in the variable store while resolution read a separate stack, so which one answered depended on who asked. - A part assignment to a system variable that only had a built-in default defined a down-value instead of extending the value. - A list literal of `$…` variables (`{$Version}`) was echoed verbatim by `interpret`'s fast path instead of being evaluated. - `clear_symbol_table` was never called, so the context symbol table survived `clear_state`. - `ComputerArithmetic`` — the reference page's own example — was missing from the standard-distribution contexts.
1 parent 1b40941 commit 671ecef

13 files changed

Lines changed: 873 additions & 19 deletions

File tree

functions.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ Sort,Sorts a list in ascending order or by an ordering function which leaves a p
249249
Series,Computes a power series expansion about a point or at Infinity,,pure,1.0,195
250250
Return,Returns a value from a definition body Do or Scan — anywhere else it stands as Return[value] and is collected like any other,,pure,1.0,196
251251
Appearance,Option symbol for specifying visual appearance,,pure,6.0,197
252-
Needs,Loads the file providing a context from a loaded paclet directory or from $Path,,effectful,1.0,198
252+
Needs,Loads the file providing a context from a loaded paclet directory or from $Path; a rule right-hand side aliases the context in $ContextAliases or (None) records nothing,,effectful,1.0,198
253253
Center,Symbol for centering alignment,,pure,1.0,199
254254
UndirectedEdge,Undirected graph edge,,pure,8.0,201
255255
Filling,Option for filling areas under curves in plots,,pure,6.0,202

src/evaluator/assignment.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,61 @@ fn normalize_symbol_lhs(lhs: &Expr) -> Expr {
10521052
lhs.clone()
10531053
}
10541054

1055+
/// The symbol an assignment ultimately writes to: `a`, `a[[1]]`, `a[k]` and
1056+
/// `a[[1, 2]]` all target `a`.
1057+
fn assignment_target_symbol(lhs: &Expr) -> Option<&str> {
1058+
match lhs {
1059+
Expr::Identifier(name) | Expr::Constant(name) => Some(name),
1060+
Expr::Part { expr, .. } => assignment_target_symbol(expr),
1061+
Expr::FunctionCall { name, .. } => Some(name),
1062+
_ => None,
1063+
}
1064+
}
1065+
1066+
/// Re-checks `$ContextAliases` once the assignment that touched it is done.
1067+
struct AliasCheck(bool);
1068+
1069+
impl Drop for AliasCheck {
1070+
fn drop(&mut self) {
1071+
if self.0 {
1072+
crate::evaluator::contexts::validate_aliases();
1073+
}
1074+
}
1075+
}
1076+
1077+
/// Give a `$…` system variable that only has a built-in default an entry in
1078+
/// the variable store, so that a *part* assignment has something to modify.
1079+
///
1080+
/// The Wolfram Language keeps these values as own-values, so
1081+
/// `$ContextAliases["c`"] = "Long`"` extends the association the variable
1082+
/// already holds. Woxi computes the defaults on demand instead, and without
1083+
/// this the assignment would fall through to defining a down-value. Only
1084+
/// container defaults are seeded — a scalar one (`$Assumptions` is `True`)
1085+
/// has no part to assign to, and wolframscript reports that as an error on
1086+
/// the value rather than on the variable.
1087+
fn seed_system_variable(name: &str) {
1088+
if !name.starts_with('$')
1089+
|| crate::ENV.with(|e| e.borrow().contains_key(name))
1090+
{
1091+
return;
1092+
}
1093+
let Some(value) = crate::evaluator::listable::get_system_variable(name)
1094+
else {
1095+
return;
1096+
};
1097+
let stored = match &value {
1098+
Expr::Association(items) => StoredValue::Association(
1099+
items
1100+
.iter()
1101+
.map(|(k, v)| (crate::syntax::expr_to_string(k), v.clone()))
1102+
.collect(),
1103+
),
1104+
Expr::List(_) => StoredValue::ExprVal(value.clone()),
1105+
_ => return,
1106+
};
1107+
crate::ENV.with(|e| e.borrow_mut().insert(name.to_string(), stored));
1108+
}
1109+
10551110
/// Heads whose `head[sym, …] = value` assignment is redirected into a
10561111
/// per-symbol storage slot (NValues, Messages, Format rules, Options, …).
10571112
/// wolframscript permits these even though the head itself is Protected,
@@ -1115,6 +1170,13 @@ pub fn set_ast(lhs: &Expr, rhs: &Expr) -> Result<Expr, InterpreterError> {
11151170
(lhs, None)
11161171
};
11171172

1173+
// Assigning `$ContextAliases`, whole or by key, re-checks the mapping the
1174+
// way the Wolfram Language does — the warnings belong after the assignment
1175+
// has landed, so they ride on the way out of every return path here.
1176+
let _alias_check = AliasCheck(
1177+
assignment_target_symbol(lhs).is_some_and(|s| s == "$ContextAliases"),
1178+
);
1179+
11181180
// Handle Entity property mutation: Entity["type", "name"]["property"] = value
11191181
if let Expr::CurriedCall { func, args } = lhs
11201182
&& let Expr::FunctionCall {
@@ -1142,6 +1204,7 @@ pub fn set_ast(lhs: &Expr, rhs: &Expr) -> Result<Expr, InterpreterError> {
11421204
} = lhs
11431205
&& head_args.len() == 1
11441206
{
1207+
seed_system_variable(head_name);
11451208
let is_assoc = crate::ENV.with(|e| {
11461209
let env = e.borrow();
11471210
matches!(env.get(head_name), Some(StoredValue::Association(_)))
@@ -1210,6 +1273,8 @@ pub fn set_ast(lhs: &Expr, rhs: &Expr) -> Result<Expr, InterpreterError> {
12101273
return Ok(rhs_value);
12111274
}
12121275

1276+
seed_system_variable(&var_name);
1277+
12131278
// Evaluate indices
12141279
let mut eval_indices = Vec::new();
12151280
for idx in &indices {

src/evaluator/contexts.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,33 @@ pub fn contexts_active() -> bool {
6060
crate::current_context() != "Global`"
6161
|| crate::current_context_path()
6262
!= vec!["System`".to_string(), "Global`".to_string()]
63+
|| crate::has_context_aliases()
64+
}
65+
66+
/// Expand a `$ContextAliases` prefix: with `mp`` aliased to `MyPkg``, the
67+
/// name `mp`Foo` is read as `MyPkg`Foo`. Only the first segment is an alias,
68+
/// and the rest of the name rides along — `mp`Sub`Foo` becomes
69+
/// `MyPkg`Sub`Foo`. A name that no alias claims comes back unchanged.
70+
pub fn expand_alias(name: &str) -> String {
71+
if !crate::has_context_aliases() {
72+
return name.to_string();
73+
}
74+
let Some(first) = name.find('`') else {
75+
return name.to_string();
76+
};
77+
// A name that starts with a backtick is relative to `$Context`; there is no
78+
// leading segment for an alias to match.
79+
if first == 0 {
80+
return name.to_string();
81+
}
82+
let prefix = &name[..=first];
83+
crate::context_aliases()
84+
.into_iter()
85+
.find(|(alias, _)| alias == prefix)
86+
.map_or_else(
87+
|| name.to_string(),
88+
|(_, target)| format!("{target}{}", &name[first + 1..]),
89+
)
6390
}
6491

6592
thread_local! {
@@ -100,6 +127,40 @@ impl Drop for ReadContext {
100127
}
101128
}
102129

130+
/// Report every `$ContextAliases` entry that cannot do its job.
131+
///
132+
/// An alias claims a context name for itself, so a context that already holds
133+
/// symbols of its own becomes unreachable once its name is an alias, and one
134+
/// that is on `$ContextPath` would be searched under a name that no longer
135+
/// means it. The Wolfram Language warns about both, and re-checks the whole
136+
/// mapping every time it changes — so a warning repeats until the entry that
137+
/// caused it is taken back out.
138+
pub fn validate_aliases() {
139+
let path = crate::current_context_path();
140+
let symbols = known_symbols();
141+
for (alias, target) in crate::context_aliases() {
142+
if path.contains(&alias) {
143+
crate::emit_message_to_stdout(&format!(
144+
"$ContextAliases::cxconflict: Warning: the alias {alias} -> {target} \
145+
conflicts with the value of $ContextPath."
146+
));
147+
}
148+
if symbols.iter().any(|(context, _)| *context == alias) {
149+
crate::emit_message_to_stdout(&format!(
150+
"$ContextAliases::cxinuse: Warning: Symbols already exist in the \
151+
context {alias}. These symbols will not be able to be accessed \
152+
while {alias} is in $ContextAliases."
153+
));
154+
}
155+
}
156+
}
157+
158+
/// Record `alias` as standing for `target` in `$ContextAliases`.
159+
pub fn set_alias(alias: &str, target: &str) {
160+
crate::set_context_alias(alias, Some(target));
161+
validate_aliases();
162+
}
163+
103164
/// The store key for `name` in `context`. `Global`` symbols are keyed by
104165
/// their short name, the way Woxi has always stored them.
105166
fn key_for(context: &str, name: &str) -> String {
@@ -231,8 +292,9 @@ pub fn resolve(name: &str) -> String {
231292
return full;
232293
}
233294
if name.contains('`') {
234-
create_symbol(name);
235-
return name.to_string();
295+
let full = expand_alias(name);
296+
create_symbol(&full);
297+
return full;
236298
}
237299
let (current, path) = read_context();
238300
for context in path {
@@ -250,9 +312,12 @@ pub fn resolve(name: &str) -> String {
250312
/// print a symbol under its visible short name, so anything that looks state
251313
/// up from a rendered name has to map it back first.
252314
pub fn resolve_existing(name: &str) -> String {
253-
if !contexts_active() || name.contains('`') || !is_user_symbol(name) {
315+
if !contexts_active() || !is_user_symbol(name) {
254316
return name.to_string();
255317
}
318+
if name.contains('`') {
319+
return expand_alias(name);
320+
}
256321
crate::current_context_path()
257322
.into_iter()
258323
.map(|context| key_for(&context, name))

src/evaluator/dispatch/evaluate_functions.rs

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,24 +1819,64 @@ fn evaluate_function_call_ast_inner(
18191819
}
18201820

18211821
// Needs["pkg`"] loads the file providing the context — from a paclet in a
1822-
// directory registered with `PacletDirectoryLoad`, or from `$Path`.
1823-
// `Needs["pkg`", "file"]` reads the named file instead. A package that
1824-
// ships with the Wolfram Language loads as a no-op, because Woxi keeps
1825-
// every built-in in one namespace.
1822+
// directory registered with `PacletDirectoryLoad`, or from `$Path` — and
1823+
// leaves the context on `$ContextPath`, so its symbols can be named
1824+
// unqualified. `Needs["pkg`", "file"]` reads the named file instead.
1825+
//
1826+
// The rule forms load the same way but keep `$ContextPath` as they found
1827+
// it: `Needs["pkg`" -> "p`"]` records `p`` as an alias for the context in
1828+
// `$ContextAliases`, so its symbols are reachable as `p`sym`, and
1829+
// `Needs["pkg`" -> None]` records nothing, leaving only the full name.
1830+
//
1831+
// A package that ships with the Wolfram Language loads as a no-op, because
1832+
// Woxi keeps every built-in in one namespace.
18261833
if name == "Needs" && (args.len() == 1 || args.len() == 2) {
18271834
let call = || {
18281835
crate::syntax::format_expr(
18291836
&unevaluated("Needs", args),
18301837
crate::syntax::ExprForm::Output,
18311838
)
18321839
};
1833-
let Expr::String(ctx) = &args[0] else {
1840+
let cxru = || {
18341841
crate::emit_message_to_stdout(&format!(
18351842
"Needs::cxru: Context or appropriately structured rule expected at \
18361843
position 1 in {}.",
18371844
call()
18381845
));
1839-
return Ok(unevaluated("Needs", args));
1846+
};
1847+
// An alias is a context of a single segment: `p``, never `p`q``.
1848+
let is_alias = |name: &str| {
1849+
crate::utils::context_segments(name).is_some_and(|s| s.len() == 1)
1850+
};
1851+
// `None` means "no rule given"; `Some(None)` is `-> None`, which asks for
1852+
// neither a context-path entry nor an alias.
1853+
let (ctx, alias) = match &args[0] {
1854+
Expr::String(ctx) => (ctx, None),
1855+
Expr::Rule {
1856+
pattern,
1857+
replacement,
1858+
} => match (pattern.as_ref(), replacement.as_ref()) {
1859+
(Expr::String(ctx), Expr::String(alias))
1860+
if crate::utils::context_segments(ctx).is_some()
1861+
&& is_alias(alias) =>
1862+
{
1863+
(ctx, Some(Some(alias)))
1864+
}
1865+
(Expr::String(ctx), Expr::Identifier(none))
1866+
if none == "None"
1867+
&& crate::utils::context_segments(ctx).is_some() =>
1868+
{
1869+
(ctx, Some(None))
1870+
}
1871+
_ => {
1872+
cxru();
1873+
return Ok(unevaluated("Needs", args));
1874+
}
1875+
},
1876+
_ => {
1877+
cxru();
1878+
return Ok(unevaluated("Needs", args));
1879+
}
18401880
};
18411881
if crate::utils::context_segments(ctx).is_none() {
18421882
crate::emit_message_to_stdout(&format!(
@@ -1846,6 +1886,23 @@ fn evaluate_function_call_ast_inner(
18461886
));
18471887
return Ok(unevaluated("Needs", args));
18481888
}
1889+
// The alias is recorded as soon as the rule has been read, so that even a
1890+
// call that goes on to fail on its second argument leaves it behind; only
1891+
// a *load* that does not produce the context takes it back out again,
1892+
// restoring whatever the alias stood for before.
1893+
let previous_alias = match alias {
1894+
Some(Some(alias)) => {
1895+
let previous = crate::context_alias_target(alias);
1896+
crate::evaluator::contexts::set_alias(alias, ctx);
1897+
Some((alias, previous))
1898+
}
1899+
_ => None,
1900+
};
1901+
let forget_alias = || {
1902+
if let Some((alias, previous)) = &previous_alias {
1903+
crate::set_context_alias(alias, previous.as_deref());
1904+
}
1905+
};
18491906
if let Some(file) = args.get(1)
18501907
&& !matches!(file, Expr::String(_))
18511908
{
@@ -1871,23 +1928,36 @@ fn evaluate_function_call_ast_inner(
18711928
Some(Expr::String(file)) => file,
18721929
_ => ctx,
18731930
};
1931+
// A rule form reaches the package by alias or by full name, so whatever
1932+
// the file did to `$ContextPath` — its own `EndPackage[]` above all —
1933+
// is undone once it has been read.
1934+
let saved_path = alias.is_some().then(crate::save_context_path);
1935+
let restore_path = || {
1936+
if let Some(saved) = saved_path.clone() {
1937+
crate::restore_context_path(saved);
1938+
}
1939+
};
18741940
let loaded = resolve_get_target(requested)
18751941
.as_deref()
18761942
.and_then(evaluate_file);
18771943
let nocont = format!(
18781944
"Needs::nocont: Context {ctx} was not created when Needs was evaluated."
18791945
);
18801946
let Some(result) = loaded else {
1947+
restore_path();
1948+
forget_alias();
18811949
crate::emit_message_to_stdout(&format!(
18821950
"Get::noopen: Cannot open {requested}."
18831951
));
18841952
crate::emit_message_to_stdout(&nocont);
18851953
return Ok(Expr::Identifier("$Failed".to_string()));
18861954
};
1955+
restore_path();
18871956
result?;
18881957
// The file loaded but never opened the context it was supposed to
18891958
// provide — wolframscript reports that and still returns Null.
18901959
if !crate::packages_list().iter().any(|pkg| pkg == ctx) {
1960+
forget_alias();
18911961
crate::emit_message_to_stdout(&nocont);
18921962
}
18931963
return Ok(Expr::Identifier("Null".to_string()));

src/evaluator/dispatch/evaluation_control.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,9 @@ pub fn dispatch_evaluation_control(
759759
// pattern without a backtick looks in the contexts on
760760
// `$ContextPath`, which is why `Names["List*"]` finds the built-ins
761761
// (they are `System`` symbols) and `Names["S`*"]` does not reach
762-
// into `S`Private``.
762+
// into `S`Private``. A leading `$ContextAliases` alias names the
763+
// context it stands for, here as anywhere else.
764+
let pattern = &crate::evaluator::contexts::expand_alias(pattern);
763765
let (context_pattern, name_pattern) = match pattern.rfind('`') {
764766
Some(last) => (
765767
Some(pattern[..=last].to_string()),

src/evaluator/dispatch/predicate_functions.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1362,7 +1362,9 @@ pub fn dispatch_predicate_functions(
13621362
}
13631363
"Contexts" if args.len() == 1 => {
13641364
let pattern = match &args[0] {
1365-
Expr::String(s) => s.clone(),
1365+
// A pattern may name a context by its `$ContextAliases` alias, which
1366+
// stands in for the context's real name here as everywhere else.
1367+
Expr::String(s) => crate::evaluator::contexts::expand_alias(s),
13661368
_ => {
13671369
return Some(Ok(unevaluated("Contexts", args)));
13681370
}

src/evaluator/listable.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ pub fn get_system_variable(name: &str) -> Option<Expr> {
847847
// Wolfram layout.
848848
#[cfg(not(target_arch = "wasm32"))]
849849
"$Path" => Some(Expr::List(
850-
crate::utils::search_path()
850+
crate::utils::default_search_path()
851851
.into_iter()
852852
.map(Expr::String)
853853
.collect(),
@@ -893,6 +893,11 @@ pub fn get_system_variable(name: &str) -> Option<Expr> {
893893
.map(Expr::String)
894894
.collect(),
895895
)),
896+
// `$ContextAliases` maps a short context onto the long one it stands
897+
// for. It starts out empty; `Needs["ctx`" -> "alias`"]` and plain
898+
// assignment both fill it in, and the value then lives in the variable
899+
// store (which takes precedence over this default).
900+
"$ContextAliases" => Some(Expr::Association(Vec::new())),
896901
// Woxi only tracks the System`/Global` baseline plus any contexts
897902
// registered by `BeginPackage[]`; wolframscript lists many kernel
898903
// packages here, but `MemberQ[$Packages, "System`"]` and

0 commit comments

Comments
 (0)