Add export statement.
This commit is contained in:
parent
89d75b1b11
commit
eb52bfa28a
26
README.md
26
README.md
@ -2044,10 +2044,30 @@ Using external modules
|
||||
[module]: #using-external-modules
|
||||
[modules]: #using-external-modules
|
||||
|
||||
Rhai allows organizing code (functions and variables) into _modules_. A module is a single script file
|
||||
with `export` statements that _exports_ certain global variables and functions as contents of the module.
|
||||
Rhai allows organizing code (functions and variables) into _modules_.
|
||||
Modules can be disabled via the [`no_module`] feature.
|
||||
|
||||
Everything exported as part of a module is constant and read-only.
|
||||
### Exporting variables and functions
|
||||
|
||||
A module is a single script (or pre-compiled `AST`) containing global variables and functions.
|
||||
The `export` statement, which can only be at global level, exposes selected variables as members of a module.
|
||||
Variables not exported are private and invisible to the outside.
|
||||
|
||||
All functions are automatically exported. Everything exported from a module is **constant** (**read-only**).
|
||||
|
||||
```rust
|
||||
// This is a module script.
|
||||
|
||||
fn inc(x) { x + 1 } // function
|
||||
|
||||
let private = 123; // variable not exported - invisible to outside
|
||||
let x = 42; // this will be exported below
|
||||
|
||||
export x; // the variable 'x' is exported under its own name
|
||||
|
||||
export x as answer; // the variable 'x' is exported under the alias 'answer'
|
||||
// another script can load this module and access 'x' as 'module::answer'
|
||||
```
|
||||
|
||||
### Importing modules
|
||||
|
||||
|
@ -1622,7 +1622,10 @@ impl Engine {
|
||||
.try_cast::<String>()
|
||||
{
|
||||
if let Some(resolver) = self.module_resolver.as_ref() {
|
||||
let module = resolver.resolve(self, &path, expr.position())?;
|
||||
// Use an empty scope to create a module
|
||||
let mut mod_scope = Scope::new();
|
||||
let module =
|
||||
resolver.resolve(self, mod_scope, &path, expr.position())?;
|
||||
|
||||
// TODO - avoid copying module name in inner block?
|
||||
let mod_name = name.as_ref().clone();
|
||||
@ -1639,6 +1642,36 @@ impl Engine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export statement
|
||||
Stmt::Export(list) => {
|
||||
for (id, id_pos, rename) in list {
|
||||
let mut found = false;
|
||||
|
||||
// Mark scope variables as public
|
||||
match scope.get_index(id) {
|
||||
Some((index, ScopeEntryType::Normal))
|
||||
| Some((index, ScopeEntryType::Constant)) => {
|
||||
let alias = rename
|
||||
.as_ref()
|
||||
.map(|(n, _)| n.clone())
|
||||
.unwrap_or_else(|| id.clone());
|
||||
scope.set_entry_alias(index, alias);
|
||||
found = true;
|
||||
}
|
||||
Some((_, ScopeEntryType::Module)) => unreachable!(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if !found {
|
||||
return Err(Box::new(EvalAltResult::ErrorVariableNotFound(
|
||||
id.into(),
|
||||
*id_pos,
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
16
src/error.rs
16
src/error.rs
@ -98,6 +98,14 @@ pub enum ParseErrorType {
|
||||
///
|
||||
/// Never appears under the `no_function` feature.
|
||||
FnMissingBody(String),
|
||||
/// An export statement has duplicated names.
|
||||
///
|
||||
/// Never appears under the `no_module` feature.
|
||||
DuplicatedExport(String),
|
||||
/// Export statement not at global level.
|
||||
///
|
||||
/// Never appears under the `no_module` feature.
|
||||
WrongExport,
|
||||
/// Assignment to a copy of a value.
|
||||
AssignmentToCopy,
|
||||
/// Assignment to an a constant variable.
|
||||
@ -147,6 +155,8 @@ impl ParseError {
|
||||
ParseErrorType::FnDuplicatedParam(_,_) => "Duplicated parameters in function declaration",
|
||||
ParseErrorType::FnMissingBody(_) => "Expecting body statement block for function declaration",
|
||||
ParseErrorType::WrongFnDefinition => "Function definitions must be at global level and cannot be inside a block or another function",
|
||||
ParseErrorType::DuplicatedExport(_) => "Duplicated variable/function in export statement",
|
||||
ParseErrorType::WrongExport => "Export statement can only appear at global level",
|
||||
ParseErrorType::AssignmentToCopy => "Only a copy of the value is change with this assignment",
|
||||
ParseErrorType::AssignmentToConstant(_) => "Cannot assign to a constant value.",
|
||||
ParseErrorType::LoopBreak => "Break statement should only be used inside a loop"
|
||||
@ -193,6 +203,12 @@ impl fmt::Display for ParseError {
|
||||
write!(f, "Duplicated parameter '{}' for function '{}'", arg, s)?
|
||||
}
|
||||
|
||||
ParseErrorType::DuplicatedExport(s) => write!(
|
||||
f,
|
||||
"Duplicated variable/function '{}' in export statement",
|
||||
s
|
||||
)?,
|
||||
|
||||
ParseErrorType::MissingToken(token, s) => write!(f, "Expecting '{}' {}", token, s)?,
|
||||
|
||||
ParseErrorType::AssignmentToConstant(s) if s.is_empty() => {
|
||||
|
@ -31,6 +31,7 @@ pub trait ModuleResolver {
|
||||
fn resolve(
|
||||
&self,
|
||||
engine: &Engine,
|
||||
scope: Scope,
|
||||
path: &str,
|
||||
pos: Position,
|
||||
) -> Result<Module, Box<EvalAltResult>>;
|
||||
@ -570,17 +571,15 @@ impl Module {
|
||||
/// use rhai::{Engine, Module};
|
||||
///
|
||||
/// let engine = Engine::new();
|
||||
/// let mut scope = Scope::new();
|
||||
/// let ast = engine.compile("let answer = 42;")?;
|
||||
/// let module = Module::eval_ast_as_new(&ast, &engine)?;
|
||||
/// let module = Module::eval_ast_as_new(scope, &ast, &engine)?;
|
||||
/// assert!(module.contains_var("answer"));
|
||||
/// assert_eq!(module.get_var_value::<i64>("answer").unwrap(), 42);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn eval_ast_as_new(ast: &AST, engine: &Engine) -> FuncReturn<Self> {
|
||||
// Use new scope
|
||||
let mut scope = Scope::new();
|
||||
|
||||
pub fn eval_ast_as_new(mut scope: Scope, ast: &AST, engine: &Engine) -> FuncReturn<Self> {
|
||||
// Run the script
|
||||
engine.eval_ast_with_scope_raw(&mut scope, &ast)?;
|
||||
|
||||
@ -589,13 +588,19 @@ impl Module {
|
||||
|
||||
scope.into_iter().for_each(
|
||||
|ScopeEntry {
|
||||
name, typ, value, ..
|
||||
name,
|
||||
typ,
|
||||
value,
|
||||
alias,
|
||||
..
|
||||
}| {
|
||||
match typ {
|
||||
// Variables left in the scope become module variables
|
||||
ScopeEntryType::Normal | ScopeEntryType::Constant => {
|
||||
module.variables.insert(name.into_owned(), value);
|
||||
// Variables with an alias left in the scope become module variables
|
||||
ScopeEntryType::Normal | ScopeEntryType::Constant if alias.is_some() => {
|
||||
module.variables.insert(*alias.unwrap(), value);
|
||||
}
|
||||
// Variables with no alias are private and not exported
|
||||
ScopeEntryType::Normal | ScopeEntryType::Constant => (),
|
||||
// Modules left in the scope become sub-modules
|
||||
ScopeEntryType::Module => {
|
||||
module
|
||||
@ -788,9 +793,10 @@ mod file {
|
||||
pub fn create_module<P: Into<PathBuf>>(
|
||||
&self,
|
||||
engine: &Engine,
|
||||
scope: Scope,
|
||||
path: &str,
|
||||
) -> Result<Module, Box<EvalAltResult>> {
|
||||
self.resolve(engine, path, Default::default())
|
||||
self.resolve(engine, scope, path, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
@ -798,6 +804,7 @@ mod file {
|
||||
fn resolve(
|
||||
&self,
|
||||
engine: &Engine,
|
||||
scope: Scope,
|
||||
path: &str,
|
||||
pos: Position,
|
||||
) -> Result<Module, Box<EvalAltResult>> {
|
||||
@ -811,7 +818,7 @@ mod file {
|
||||
.compile_file(file_path)
|
||||
.map_err(|err| EvalAltResult::set_position(err, pos))?;
|
||||
|
||||
Module::eval_ast_as_new(&ast, engine)
|
||||
Module::eval_ast_as_new(scope, &ast, engine)
|
||||
.map_err(|err| EvalAltResult::set_position(err, pos))
|
||||
}
|
||||
}
|
||||
@ -938,6 +945,7 @@ mod stat {
|
||||
fn resolve(
|
||||
&self,
|
||||
_: &Engine,
|
||||
_: Scope,
|
||||
path: &str,
|
||||
pos: Position,
|
||||
) -> Result<Module, Box<EvalAltResult>> {
|
||||
|
@ -284,6 +284,8 @@ pub enum Stmt {
|
||||
ReturnWithVal(Option<Box<Expr>>, ReturnType, Position),
|
||||
/// import expr as module
|
||||
Import(Box<Expr>, Box<String>, Position),
|
||||
/// expr id as name, ...
|
||||
Export(Vec<(String, Position, Option<(String, Position)>)>),
|
||||
}
|
||||
|
||||
impl Stmt {
|
||||
@ -302,6 +304,8 @@ impl Stmt {
|
||||
Stmt::IfThenElse(expr, _, _) | Stmt::Expr(expr) => expr.position(),
|
||||
|
||||
Stmt::While(_, stmt) | Stmt::Loop(stmt) | Stmt::For(_, _, stmt) => stmt.position(),
|
||||
|
||||
Stmt::Export(list) => list.get(0).unwrap().1,
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,6 +324,7 @@ impl Stmt {
|
||||
Stmt::Let(_, _, _)
|
||||
| Stmt::Const(_, _, _)
|
||||
| Stmt::Import(_, _, _)
|
||||
| Stmt::Export(_)
|
||||
| Stmt::Expr(_)
|
||||
| Stmt::Continue(_)
|
||||
| Stmt::Break(_)
|
||||
@ -344,6 +349,7 @@ impl Stmt {
|
||||
Stmt::Block(statements, _) => statements.iter().all(Stmt::is_pure),
|
||||
Stmt::Continue(_) | Stmt::Break(_) | Stmt::ReturnWithVal(_, _, _) => false,
|
||||
Stmt::Import(_, _, _) => false,
|
||||
Stmt::Export(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1970,6 +1976,63 @@ fn parse_import<'a>(
|
||||
Ok(Stmt::Import(Box::new(expr), Box::new(name), pos))
|
||||
}
|
||||
|
||||
/// Parse an export statement.
|
||||
fn parse_export<'a>(input: &mut Peekable<TokenIterator<'a>>) -> Result<Stmt, Box<ParseError>> {
|
||||
eat_token(input, Token::Export);
|
||||
|
||||
let mut exports = Vec::new();
|
||||
|
||||
loop {
|
||||
let (id, id_pos) = match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => (s.clone(), pos),
|
||||
(Token::LexError(err), pos) => {
|
||||
return Err(PERR::BadInput(err.to_string()).into_err(pos))
|
||||
}
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
};
|
||||
|
||||
let rename = if match_token(input, Token::As)? {
|
||||
match input.next().unwrap() {
|
||||
(Token::Identifier(s), pos) => Some((s.clone(), pos)),
|
||||
(_, pos) => return Err(PERR::VariableExpected.into_err(pos)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
exports.push((id, id_pos, rename));
|
||||
|
||||
match input.peek().unwrap() {
|
||||
(Token::Comma, _) => {
|
||||
eat_token(input, Token::Comma);
|
||||
}
|
||||
(Token::Identifier(_), pos) => {
|
||||
return Err(PERR::MissingToken(
|
||||
Token::Comma.into(),
|
||||
"to separate the list of exports".into(),
|
||||
)
|
||||
.into_err(*pos))
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicating parameters
|
||||
exports
|
||||
.iter()
|
||||
.enumerate()
|
||||
.try_for_each(|(i, (p1, _, _))| {
|
||||
exports
|
||||
.iter()
|
||||
.skip(i + 1)
|
||||
.find(|(p2, _, _)| p2 == p1)
|
||||
.map_or_else(|| Ok(()), |(p2, pos, _)| Err((p2, *pos)))
|
||||
})
|
||||
.map_err(|(p, pos)| PERR::DuplicatedExport(p.to_string()).into_err(pos))?;
|
||||
|
||||
Ok(Stmt::Export(exports))
|
||||
}
|
||||
|
||||
/// Parse a statement block.
|
||||
fn parse_block<'a>(
|
||||
input: &mut Peekable<TokenIterator<'a>>,
|
||||
@ -1995,7 +2058,7 @@ fn parse_block<'a>(
|
||||
|
||||
while !match_token(input, Token::RightBrace)? {
|
||||
// Parse statements inside the block
|
||||
let stmt = parse_stmt(input, stack, breakable, allow_stmt_expr)?;
|
||||
let stmt = parse_stmt(input, stack, breakable, false, allow_stmt_expr)?;
|
||||
|
||||
// See if it needs a terminating semicolon
|
||||
let need_semicolon = !stmt.is_self_terminated();
|
||||
@ -2053,6 +2116,7 @@ fn parse_stmt<'a>(
|
||||
input: &mut Peekable<TokenIterator<'a>>,
|
||||
stack: &mut Stack,
|
||||
breakable: bool,
|
||||
is_global: bool,
|
||||
allow_stmt_expr: bool,
|
||||
) -> Result<Stmt, Box<ParseError>> {
|
||||
let (token, pos) = match input.peek().unwrap() {
|
||||
@ -2113,6 +2177,12 @@ fn parse_stmt<'a>(
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
Token::Import => parse_import(input, stack, allow_stmt_expr),
|
||||
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
Token::Export if !is_global => Err(PERR::WrongExport.into_err(*pos)),
|
||||
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
Token::Export => parse_export(input),
|
||||
|
||||
_ => parse_expr_stmt(input, stack, allow_stmt_expr),
|
||||
}
|
||||
}
|
||||
@ -2123,7 +2193,7 @@ fn parse_fn<'a>(
|
||||
stack: &mut Stack,
|
||||
allow_stmt_expr: bool,
|
||||
) -> Result<FnDef, Box<ParseError>> {
|
||||
let pos = input.next().expect("should be fn").1;
|
||||
let pos = eat_token(input, Token::Fn);
|
||||
|
||||
let name = match input.next().unwrap() {
|
||||
(Token::Identifier(s), _) => s,
|
||||
@ -2258,7 +2328,7 @@ fn parse_global_level<'a>(
|
||||
}
|
||||
}
|
||||
// Actual statement
|
||||
let stmt = parse_stmt(input, &mut stack, false, true)?;
|
||||
let stmt = parse_stmt(input, &mut stack, false, true, true)?;
|
||||
|
||||
let need_semicolon = !stmt.is_self_terminated();
|
||||
|
||||
|
17
src/scope.rs
17
src/scope.rs
@ -30,6 +30,8 @@ pub struct Entry<'a> {
|
||||
pub typ: EntryType,
|
||||
/// Current value of the entry.
|
||||
pub value: Dynamic,
|
||||
/// Alias of the entry.
|
||||
pub alias: Option<Box<String>>,
|
||||
/// A constant expression if the initial value matches one of the recognized types.
|
||||
pub expr: Option<Box<Expr>>,
|
||||
}
|
||||
@ -248,6 +250,7 @@ impl<'a> Scope<'a> {
|
||||
self.0.push(Entry {
|
||||
name: name.into(),
|
||||
typ: entry_type,
|
||||
alias: None,
|
||||
value: value.into(),
|
||||
expr,
|
||||
});
|
||||
@ -412,16 +415,15 @@ impl<'a> Scope<'a> {
|
||||
/// Get a mutable reference to an entry in the Scope.
|
||||
pub(crate) fn get_mut(&mut self, index: usize) -> (&mut Dynamic, EntryType) {
|
||||
let entry = self.0.get_mut(index).expect("invalid index in Scope");
|
||||
|
||||
// assert_ne!(
|
||||
// entry.typ,
|
||||
// EntryType::Constant,
|
||||
// "get mut of constant entry"
|
||||
// );
|
||||
|
||||
(&mut entry.value, entry.typ)
|
||||
}
|
||||
|
||||
/// Update the access type of an entry in the Scope.
|
||||
pub(crate) fn set_entry_alias(&mut self, index: usize, alias: String) {
|
||||
let entry = self.0.get_mut(index).expect("invalid index in Scope");
|
||||
entry.alias = Some(Box::new(alias));
|
||||
}
|
||||
|
||||
/// Get an iterator to entries in the Scope.
|
||||
pub(crate) fn into_iter(self) -> impl Iterator<Item = Entry<'a>> {
|
||||
self.0.into_iter()
|
||||
@ -439,6 +441,7 @@ impl<'a, K: Into<Cow<'a, str>>> iter::Extend<(K, EntryType, Dynamic)> for Scope<
|
||||
.extend(iter.into_iter().map(|(name, typ, value)| Entry {
|
||||
name: name.into(),
|
||||
typ,
|
||||
alias: None,
|
||||
value: value.into(),
|
||||
expr: None,
|
||||
}));
|
||||
|
Loading…
Reference in New Issue
Block a user