Add export statement.

This commit is contained in:
Stephen Chung 2020-05-08 16:49:24 +08:00
parent 89d75b1b11
commit eb52bfa28a
6 changed files with 175 additions and 25 deletions

View File

@ -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

View File

@ -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())
}
}
}

View File

@ -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() => {

View File

@ -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>> {

View File

@ -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();

View File

@ -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,
}));