Skip to content

Commit

Permalink
Merge pull request #873 from schungx/master
Browse files Browse the repository at this point in the history
Enable closures in `$func$`
  • Loading branch information
schungx committed May 11, 2024
2 parents e1cd558 + cc25d83 commit d70af60
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 76 deletions.
141 changes: 66 additions & 75 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1424,40 +1424,7 @@ impl Engine {
}
#[cfg(not(feature = "no_function"))]
Token::Pipe | Token::Or if settings.has_option(LangOptions::ANON_FN) => {
let (expr, fn_def, _externals) = self.parse_anon_fn(
state,
settings,
false,
#[cfg(not(feature = "no_closure"))]
true,
)?;

#[cfg(not(feature = "no_closure"))]
for Ident { name, pos } in &_externals {
let (index, is_func) = self.access_var(state, name, *pos);

if !is_func
&& index.is_none()
&& !settings.has_flag(ParseSettingFlags::CLOSURE_SCOPE)
&& settings.has_option(LangOptions::STRICT_VAR)
&& !state
.external_constants
.map_or(false, |scope| scope.contains(name))
{
// If the parent scope is not inside another capturing closure
// then we can conclude that the captured variable doesn't exist.
// Under Strict Variables mode, this is not allowed.
return Err(PERR::VariableUndefined(name.to_string()).into_err(*pos));
}
}

let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len());
state.lib.insert(hash_script, fn_def);

#[cfg(not(feature = "no_closure"))]
let expr = self.make_curry_from_externals(state, expr, _externals, settings.pos);

expr
self.parse_anon_fn(state, settings, false)?
}

// Interpolated string
Expand Down Expand Up @@ -2532,19 +2499,7 @@ impl Engine {
.into_err(*fwd_pos))
}
};

let (expr, fn_def, _) = self.parse_anon_fn(
state,
settings,
skip,
#[cfg(not(feature = "no_closure"))]
false,
)?;

let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len());
state.lib.insert(hash_script, fn_def);

inputs.push(expr);
inputs.push(self.parse_anon_fn(state, settings, skip)?);
let keyword = self.get_interned_string(CUSTOM_SYNTAX_MARKER_FUNC);
segments.push(keyword.clone());
tokens.push(keyword);
Expand Down Expand Up @@ -2631,6 +2586,9 @@ impl Engine {
// If the last symbol is `;` or `}`, it is self-terminating
KEYWORD_SEMICOLON | KEYWORD_CLOSE_BRACE
);
// It is self-terminating if the last symbol is a block
#[cfg(not(feature = "no_function"))]
let self_terminated = required_token == CUSTOM_SYNTAX_MARKER_FUNC || self_terminated;

Ok(Expr::Custom(
crate::ast::CustomExpr {
Expand Down Expand Up @@ -3711,8 +3669,7 @@ impl Engine {
state: &mut ParseState,
settings: ParseSettings,
skip_parameters: bool,
#[cfg(not(feature = "no_closure"))] allow_capture: bool,
) -> ParseResult<(Expr, Shared<ScriptFuncDef>, ThinVec<Ident>)> {
) -> ParseResult<Expr> {
// Build new parse state
let new_state = &mut ParseState::new(
state.external_constants,
Expand All @@ -3732,25 +3689,6 @@ impl Engine {
new_state.global_imports.extend(state.imports.clone());
}

// Brand new options
#[cfg(not(feature = "no_closure"))]
let options = self.options & !LangOptions::STRICT_VAR; // a capturing closure can access variables not defined locally, so turn off Strict Variables mode
#[cfg(feature = "no_closure")]
let options = self.options | (settings.options & LangOptions::STRICT_VAR);

// Brand new flags, turn on function scope and closure scope
let flags = ParseSettingFlags::FN_SCOPE
| ParseSettingFlags::CLOSURE_SCOPE
| (settings.flags
& (ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES
| ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS));

let new_settings = ParseSettings {
flags,
options,
..settings
};

let mut params_list = StaticVec::<ImmutableString>::new_const();

// Parse parameters
Expand Down Expand Up @@ -3797,24 +3735,48 @@ impl Engine {
}
}

// Brand new options
#[cfg(not(feature = "no_closure"))]
let options = self.options & !LangOptions::STRICT_VAR; // a capturing closure can access variables not defined locally, so turn off Strict Variables mode
#[cfg(feature = "no_closure")]
let options = self.options | (settings.options & LangOptions::STRICT_VAR);

// Brand new flags, turn on function scope and closure scope
let flags = ParseSettingFlags::FN_SCOPE
| ParseSettingFlags::CLOSURE_SCOPE
| (settings.flags
& (ParseSettingFlags::DISALLOW_UNQUOTED_MAP_PROPERTIES
| ParseSettingFlags::DISALLOW_STATEMENTS_IN_BLOCKS));

let new_settings = ParseSettings {
flags,
options,
..settings
};

// Parse function body
let body = self.parse_stmt(new_state, new_settings.level_up()?)?;

let _ = new_settings; // Make sure it doesn't leak into code below

// External variables may need to be processed in a consistent order,
// so extract them into a list.
#[cfg(not(feature = "no_closure"))]
let (mut params, externals) = if allow_capture {
let (mut params, _externals) = {
let externals = std::mem::take(&mut new_state.external_vars);

let mut params = FnArgsVec::with_capacity(params_list.len() + externals.len());
params.extend(externals.iter().map(|Ident { name, .. }| name.clone()));

(params, externals)
} else {
(FnArgsVec::with_capacity(params_list.len()), ThinVec::new())
};
#[cfg(feature = "no_closure")]
let (mut params, externals) = (FnArgsVec::with_capacity(params_list.len()), ThinVec::new());
let (mut params, _externals) = (
FnArgsVec::with_capacity(params_list.len()),
ThinVec::<Ident>::new(),
);

let _ = new_state; // Make sure it doesn't leak into code below

params.append(&mut params_list);

Expand All @@ -3826,7 +3788,7 @@ impl Engine {
let fn_name = self.get_interned_string(make_anonymous_fn(hash));

// Define the function
let script = Shared::new(ScriptFuncDef {
let fn_def = Shared::new(ScriptFuncDef {
name: fn_name.clone(),
access: crate::FnAccess::Public,
#[cfg(not(feature = "no_object"))]
Expand All @@ -3838,16 +3800,45 @@ impl Engine {
comments: <_>::default(),
});

// Define the function pointer
let fn_ptr = crate::FnPtr {
name: fn_name,
curry: ThinVec::new(),
environ: None,
#[cfg(not(feature = "no_function"))]
fn_def: Some(script.clone()),
fn_def: Some(fn_def.clone()),
};

let expr = Expr::DynamicConstant(Box::new(fn_ptr.into()), new_settings.pos);

Ok((expr, script, externals))
// Finished with `new_state` here. Revert back to using `state`.

#[cfg(not(feature = "no_closure"))]
for Ident { name, pos } in &_externals {
let (index, is_func) = self.access_var(state, name, *pos);

if !is_func
&& index.is_none()
&& !settings.has_flag(ParseSettingFlags::CLOSURE_SCOPE)
&& settings.has_option(LangOptions::STRICT_VAR)
&& !state
.external_constants
.map_or(false, |scope| scope.contains(name))
{
// If the parent scope is not inside another capturing closure
// then we can conclude that the captured variable doesn't exist.
// Under Strict Variables mode, this is not allowed.
return Err(PERR::VariableUndefined(name.to_string()).into_err(*pos));
}
}

let hash_script = calc_fn_hash(None, &fn_def.name, fn_def.params.len());
state.lib.insert(hash_script, fn_def);

#[cfg(not(feature = "no_closure"))]
let expr = self.make_curry_from_externals(state, expr, _externals, settings.pos);

Ok(expr)
}

/// Parse a global level expression.
Expand Down
16 changes: 15 additions & 1 deletion tests/custom_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,25 @@ fn test_custom_syntax_func() {
let mut engine = Engine::new();

engine
.register_custom_syntax(["hello", "$func$"], false, |_, inputs| Ok(inputs[0].get_literal_value::<FnPtr>().unwrap().into()))
.register_custom_syntax(["hello", "$func$"], false, |context, inputs| context.eval_expression_tree(&inputs[0]))
.unwrap();

assert_eq!(engine.eval::<INT>("call(hello |x| { x + 1 }, 41)").unwrap(), 42);
assert_eq!(engine.eval::<INT>("call(hello { 42 })").unwrap(), 42);

#[cfg(not(feature = "no_closure"))]
assert_eq!(
engine
.eval::<INT>(
"
let a = 1;
let f = hello |x| { x + a };
call(f, 41)
"
)
.unwrap(),
42
);
}

#[test]
Expand Down

0 comments on commit d70af60

Please sign in to comment.