feat: args expansion

This commit is contained in:
Shunsuke Shibayama 2024-08-20 00:05:42 +09:00
parent 33b0212002
commit d433bcbcce
10 changed files with 270 additions and 34 deletions

View file

@ -113,8 +113,10 @@ impl_u8_enum! {Opcode309;
BUILD_TUPLE_UNPACK_WITH_CALL = 158, BUILD_TUPLE_UNPACK_WITH_CALL = 158,
LOAD_METHOD = 160, LOAD_METHOD = 160,
CALL_METHOD = 161, CALL_METHOD = 161,
CALL_FINALLY = 162, LIST_EXTEND = 162,
POP_FINALLY = 163, SET_UPDATE = 163,
DICT_MERGE = 164,
DICT_UPDATE = 165,
// Erg-specific opcodes (must have a unary `ERG_`) // Erg-specific opcodes (must have a unary `ERG_`)
// Define in descending order from 219, 255 // Define in descending order from 219, 255
ERG_POP_NTH = 196, ERG_POP_NTH = 196,

View file

@ -111,6 +111,9 @@ impl_u8_enum! {Opcode310;
LOAD_METHOD = 160, LOAD_METHOD = 160,
CALL_METHOD = 161, CALL_METHOD = 161,
LIST_EXTEND = 162, LIST_EXTEND = 162,
SET_UPDATE = 163,
DICT_MERGE = 164,
DICT_UPDATE = 165,
// Erg-specific opcodes (must have a unary `ERG_`) // Erg-specific opcodes (must have a unary `ERG_`)
// Define in descending order from 219, 255 // Define in descending order from 219, 255
ERG_POP_NTH = 196, ERG_POP_NTH = 196,

View file

@ -101,6 +101,9 @@ impl_u8_enum! {Opcode311;
BUILD_STRING = 157, BUILD_STRING = 157,
LOAD_METHOD = 160, LOAD_METHOD = 160,
LIST_EXTEND = 162, LIST_EXTEND = 162,
SET_UPDATE = 163,
DICT_MERGE = 164,
DICT_UPDATE = 165,
PRECALL = 166, PRECALL = 166,
CALL = 171, CALL = 171,
KW_NAMES = 172, KW_NAMES = 172,

View file

@ -609,7 +609,7 @@ impl PyCodeGenerator {
} }
fn stack_dec_n(&mut self, n: usize) { fn stack_dec_n(&mut self, n: usize) {
if n > 0 && self.stack_len() == 0 { if n as u32 > self.stack_len() {
let lasti = self.lasti(); let lasti = self.lasti();
let last = self.cur_block_codeobj().code.last().unwrap(); let last = self.cur_block_codeobj().code.last().unwrap();
self.crash(&format!( self.crash(&format!(
@ -2825,6 +2825,19 @@ impl PyCodeGenerator {
self.write_instr(Opcode310::LIST_TO_TUPLE); self.write_instr(Opcode310::LIST_TO_TUPLE);
self.write_arg(0); self.write_arg(0);
} }
self.stack_dec();
}
fn emit_kw_var_args_311(&mut self, pos_len: usize, kw_var: &PosArg) {
self.write_instr(BUILD_TUPLE);
self.write_arg(pos_len);
self.stack_dec_n(pos_len.saturating_sub(1));
self.write_instr(BUILD_MAP);
self.write_arg(0);
self.emit_expr(kw_var.expr.clone());
self.write_instr(Opcode311::DICT_MERGE);
self.write_arg(1);
self.stack_dec();
} }
fn emit_var_args_308(&mut self, pos_len: usize, var_args: &PosArg) { fn emit_var_args_308(&mut self, pos_len: usize, var_args: &PosArg) {
@ -2839,6 +2852,14 @@ impl PyCodeGenerator {
} }
} }
fn emit_kw_var_args_308(&mut self, pos_len: usize, kw_var: &PosArg) {
self.write_instr(BUILD_TUPLE);
self.write_arg(pos_len);
self.emit_expr(kw_var.expr.clone());
self.stack_dec_n(pos_len.saturating_sub(1));
self.stack_dec();
}
fn emit_args_311(&mut self, mut args: Args, kind: AccessKind) { fn emit_args_311(&mut self, mut args: Args, kind: AccessKind) {
let argc = args.len(); let argc = args.len();
let pos_len = args.pos_args.len(); let pos_len = args.pos_args.len();
@ -2857,6 +2878,13 @@ impl PyCodeGenerator {
kws.push(ValueObj::Str(arg.keyword.content)); kws.push(ValueObj::Str(arg.keyword.content));
self.emit_expr(arg.expr); self.emit_expr(arg.expr);
} }
if let Some(kw_var) = &args.kw_var {
if self.py_version.minor >= Some(10) {
self.emit_kw_var_args_311(pos_len, kw_var);
} else {
self.emit_kw_var_args_308(pos_len, kw_var);
}
}
let kwsc = if !kws.is_empty() { let kwsc = if !kws.is_empty() {
self.emit_call_kw_instr(argc, kws); self.emit_call_kw_instr(argc, kws);
#[allow(clippy::bool_to_int_with_if)] #[allow(clippy::bool_to_int_with_if)]
@ -2866,9 +2894,9 @@ impl PyCodeGenerator {
1 1
} }
} else { } else {
if args.var_args.is_some() { if args.var_args.is_some() || args.kw_var.is_some() {
self.write_instr(CALL_FUNCTION_EX); self.write_instr(CALL_FUNCTION_EX);
if kws.is_empty() { if kws.is_empty() && args.kw_var.is_none() {
self.write_arg(0); self.write_arg(0);
} else { } else {
self.write_arg(1); self.write_arg(1);

View file

@ -1081,12 +1081,14 @@ impl Context {
} }
// returns callee's type, not the return type // returns callee's type, not the return type
#[allow(clippy::too_many_arguments)]
fn search_callee_info( fn search_callee_info(
&self, &self,
obj: &hir::Expr, obj: &hir::Expr,
attr_name: &Option<Identifier>, attr_name: &Option<Identifier>,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
input: &Input, input: &Input,
namespace: &Context, namespace: &Context,
) -> SingleTyCheckResult<VarInfo> { ) -> SingleTyCheckResult<VarInfo> {
@ -1107,10 +1109,24 @@ impl Context {
if let Some(attr_name) = attr_name.as_ref() { if let Some(attr_name) = attr_name.as_ref() {
let mut vi = let mut vi =
self.search_method_info(obj, attr_name, pos_args, kw_args, input, namespace)?; self.search_method_info(obj, attr_name, pos_args, kw_args, input, namespace)?;
vi.t = self.resolve_overload(obj, vi.t, pos_args, kw_args, attr_name)?; vi.t = self.resolve_overload(
obj,
vi.t,
pos_args,
kw_args,
(var_args, kw_var_args),
attr_name,
)?;
Ok(vi) Ok(vi)
} else { } else {
let t = self.resolve_overload(obj, obj.t(), pos_args, kw_args, obj)?; let t = self.resolve_overload(
obj,
obj.t(),
pos_args,
kw_args,
(var_args, kw_var_args),
obj,
)?;
Ok(VarInfo { Ok(VarInfo {
t, t,
..VarInfo::default() ..VarInfo::default()
@ -1155,6 +1171,7 @@ impl Context {
instance: Type, instance: Type,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
loc: &impl Locational, loc: &impl Locational,
) -> SingleTyCheckResult<Type> { ) -> SingleTyCheckResult<Type> {
let intersecs = instance.intersection_types(); let intersecs = instance.intersection_types();
@ -1195,7 +1212,15 @@ impl Context {
if self.subtype_of(ty, &input_t) { if self.subtype_of(ty, &input_t) {
if let Ok(instance) = self.instantiate(ty.clone(), obj) { if let Ok(instance) = self.instantiate(ty.clone(), obj) {
let subst = self let subst = self
.substitute_call(obj, &None, &instance, pos_args, kw_args, self) .substitute_call(
obj,
&None,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
self,
)
.is_ok(); .is_ok();
let eval = self.eval_t_params(instance, self.level, obj).is_ok(); let eval = self.eval_t_params(instance, self.level, obj).is_ok();
if subst && eval { if subst && eval {
@ -1641,7 +1666,7 @@ impl Context {
) )
})?; })?;
let op = hir::Expr::Accessor(hir::Accessor::private(symbol, t)); let op = hir::Expr::Accessor(hir::Accessor::private(symbol, t));
self.get_call_t(&op, &None, args, &[], input, namespace) self.get_call_t(&op, &None, args, &[], (None, None), input, namespace)
.map_err(|(_, errs)| { .map_err(|(_, errs)| {
let hir::Expr::Accessor(hir::Accessor::Ident(op_ident)) = op else { let hir::Expr::Accessor(hir::Accessor::Ident(op_ident)) = op else {
return errs; return errs;
@ -1695,7 +1720,7 @@ impl Context {
) )
})?; })?;
let op = hir::Expr::Accessor(hir::Accessor::private(symbol, vi)); let op = hir::Expr::Accessor(hir::Accessor::private(symbol, vi));
self.get_call_t(&op, &None, args, &[], input, namespace) self.get_call_t(&op, &None, args, &[], (None, None), input, namespace)
.map_err(|(_, errs)| { .map_err(|(_, errs)| {
let hir::Expr::Accessor(hir::Accessor::Ident(op_ident)) = op else { let hir::Expr::Accessor(hir::Accessor::Ident(op_ident)) = op else {
return errs; return errs;
@ -1787,6 +1812,7 @@ impl Context {
/// ↓ don't substitute `Int` to `self` /// ↓ don't substitute `Int` to `self`
/// substitute_call(obj: Int, instance: ((self: Int, other: Int) -> Int), [1, 2]) => instance: (Int, Int) -> Int /// substitute_call(obj: Int, instance: ((self: Int, other: Int) -> Int), [1, 2]) => instance: (Int, Int) -> Int
/// ``` /// ```
#[allow(clippy::too_many_arguments)]
fn substitute_call( fn substitute_call(
&self, &self,
obj: &hir::Expr, obj: &hir::Expr,
@ -1794,6 +1820,7 @@ impl Context {
instance: &Type, instance: &Type,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
namespace: &Context, namespace: &Context,
) -> TyCheckResult<SubstituteResult> { ) -> TyCheckResult<SubstituteResult> {
match instance { match instance {
@ -1803,6 +1830,7 @@ impl Context {
fv.unsafe_crack(), fv.unsafe_crack(),
pos_args, pos_args,
kw_args, kw_args,
(var_args, kw_var_args),
namespace, namespace,
), ),
Type::FreeVar(fv) => { Type::FreeVar(fv) => {
@ -1816,12 +1844,24 @@ impl Context {
if instance.is_quantified_subr() { if instance.is_quantified_subr() {
let instance = self.instantiate(instance.clone(), obj)?; let instance = self.instantiate(instance.clone(), obj)?;
self.substitute_call( self.substitute_call(
obj, attr_name, &instance, pos_args, kw_args, namespace, obj,
attr_name,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
)?; )?;
return Ok(SubstituteResult::Coerced(instance)); return Ok(SubstituteResult::Coerced(instance));
} else if get_hash(instance) != hash { } else if get_hash(instance) != hash {
return self.substitute_call( return self.substitute_call(
obj, attr_name, instance, pos_args, kw_args, namespace, obj,
attr_name,
instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
); );
} }
} }
@ -1852,17 +1892,36 @@ impl Context {
Ok(SubstituteResult::Ok) Ok(SubstituteResult::Ok)
} }
} }
Type::Refinement(refine) => { Type::Refinement(refine) => self.substitute_call(
self.substitute_call(obj, attr_name, &refine.t, pos_args, kw_args, namespace) obj,
} attr_name,
&refine.t,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
),
// instance must be instantiated // instance must be instantiated
Type::Quantified(_) => unreachable_error!(TyCheckErrors, TyCheckError, self), Type::Quantified(_) => unreachable_error!(TyCheckErrors, TyCheckError, self),
Type::Subr(subr) => { Type::Subr(subr) => {
let res = self.substitute_subr_call(obj, attr_name, subr, pos_args, kw_args); let res = self.substitute_subr_call(
obj,
attr_name,
subr,
pos_args,
kw_args,
(var_args, kw_var_args),
);
// TODO: change polymorphic type syntax // TODO: change polymorphic type syntax
if res.is_err() && subr.return_t.is_class_type() { if res.is_err() && subr.return_t.is_class_type() {
self.substitute_dunder_call( self.substitute_dunder_call(
obj, attr_name, instance, pos_args, kw_args, namespace, obj,
attr_name,
instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
) )
.or(res) .or(res)
} else { } else {
@ -1870,14 +1929,34 @@ impl Context {
} }
} }
Type::And(_, _) => { Type::And(_, _) => {
let instance = let instance = self.resolve_overload(
self.resolve_overload(obj, instance.clone(), pos_args, kw_args, &obj)?; obj,
self.substitute_call(obj, attr_name, &instance, pos_args, kw_args, namespace) instance.clone(),
pos_args,
kw_args,
(var_args, kw_var_args),
&obj,
)?;
self.substitute_call(
obj,
attr_name,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
)
} }
Type::Failure => Ok(SubstituteResult::Ok), Type::Failure => Ok(SubstituteResult::Ok),
_ => { _ => self.substitute_dunder_call(
self.substitute_dunder_call(obj, attr_name, instance, pos_args, kw_args, namespace) obj,
} attr_name,
instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
),
} }
} }
@ -1888,6 +1967,7 @@ impl Context {
subr: &SubrType, subr: &SubrType,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
) -> TyCheckResult<SubstituteResult> { ) -> TyCheckResult<SubstituteResult> {
let mut errs = TyCheckErrors::empty(); let mut errs = TyCheckErrors::empty();
// method: obj: 1, subr: (self: Int, other: Int) -> Int // method: obj: 1, subr: (self: Int, other: Int) -> Int
@ -1911,7 +1991,10 @@ impl Context {
} else { } else {
subr.non_default_params.len() + subr.default_params.len() subr.non_default_params.len() + subr.default_params.len()
}; };
if (params_len < pos_args.len() || params_len < pos_args.len() + kw_args.len()) let there_var = var_args.is_some() || kw_var_args.is_some();
if (params_len < pos_args.len()
|| params_len < pos_args.len() + kw_args.len()
|| (params_len == pos_args.len() + kw_args.len() && there_var))
&& subr.is_no_var() && subr.is_no_var()
{ {
return Err(self.gen_too_many_args_error(&callee, subr, is_method, pos_args, kw_args)); return Err(self.gen_too_many_args_error(&callee, subr, is_method, pos_args, kw_args));
@ -2047,7 +2130,59 @@ impl Context {
.into() .into()
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !missing_params.is_empty() { if let Some(var_args) = var_args {
if !self.subtype_of(
var_args.expr.ref_t(),
&poly("Iterable", vec![TyParam::t(Obj)]),
) {
let err = TyCheckError::type_mismatch_error(
self.cfg.input.clone(),
line!() as usize,
var_args.loc(),
self.caused_by(),
"_",
None,
&poly("Iterable", vec![TyParam::t(Obj)]),
var_args.expr.ref_t(),
None,
None,
);
errs.push(err);
}
}
if let Some(kw_var_args) = kw_var_args {
if !self.subtype_of(
kw_var_args.expr.ref_t(),
&poly("Mapping", vec![TyParam::t(Obj), TyParam::t(Obj)]),
) {
let err = TyCheckError::type_mismatch_error(
self.cfg.input.clone(),
line!() as usize,
kw_var_args.loc(),
self.caused_by(),
"_",
None,
&poly("Mapping", vec![TyParam::t(Obj), TyParam::t(Obj)]),
kw_var_args.expr.ref_t(),
None,
None,
);
errs.push(err);
}
}
if missing_params.is_empty() && (var_args.is_some() || kw_var_args.is_some()) {
return Err(TyCheckErrors::from(TyCheckError::too_many_args_error(
self.cfg.input.clone(),
line!() as usize,
callee.loc(),
&callee.to_string(),
self.caused_by(),
params_len,
pos_args.len(),
kw_args.len(),
)));
}
if !missing_params.is_empty() && var_args.is_none() && kw_var_args.is_none() {
return Err(TyCheckErrors::from(TyCheckError::args_missing_error( return Err(TyCheckErrors::from(TyCheckError::args_missing_error(
self.cfg.input.clone(), self.cfg.input.clone(),
line!() as usize, line!() as usize,
@ -2068,6 +2203,7 @@ impl Context {
} }
} }
#[allow(clippy::too_many_arguments)]
fn substitute_dunder_call( fn substitute_dunder_call(
&self, &self,
obj: &hir::Expr, obj: &hir::Expr,
@ -2075,6 +2211,7 @@ impl Context {
instance: &Type, instance: &Type,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
namespace: &Context, namespace: &Context,
) -> TyCheckResult<SubstituteResult> { ) -> TyCheckResult<SubstituteResult> {
let ctxs = self let ctxs = self
@ -2121,9 +2258,15 @@ impl Context {
)?; )?;
} }
let instance = self.instantiate_def_type(&call_vi.t)?; let instance = self.instantiate_def_type(&call_vi.t)?;
let instance = match self let instance = match self.substitute_call(
.substitute_call(obj, attr_name, &instance, pos_args, kw_args, namespace)? obj,
{ attr_name,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
)? {
SubstituteResult::__Call__(instance) SubstituteResult::__Call__(instance)
| SubstituteResult::Coerced(instance) => instance, | SubstituteResult::Coerced(instance) => instance,
SubstituteResult::Ok => instance, SubstituteResult::Ok => instance,
@ -2138,9 +2281,15 @@ impl Context {
}) })
{ {
let instance = self.instantiate_def_type(&call_vi.t)?; let instance = self.instantiate_def_type(&call_vi.t)?;
let instance = match self let instance = match self.substitute_call(
.substitute_call(obj, attr_name, &instance, pos_args, kw_args, namespace)? obj,
{ attr_name,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
)? {
SubstituteResult::__Call__(instance) | SubstituteResult::Coerced(instance) => { SubstituteResult::__Call__(instance) | SubstituteResult::Coerced(instance) => {
instance instance
} }
@ -2469,12 +2618,14 @@ impl Context {
Ok(res) Ok(res)
} }
#[allow(clippy::too_many_arguments)]
pub(crate) fn get_call_t( pub(crate) fn get_call_t(
&self, &self,
obj: &hir::Expr, obj: &hir::Expr,
attr_name: &Option<Identifier>, attr_name: &Option<Identifier>,
pos_args: &[hir::PosArg], pos_args: &[hir::PosArg],
kw_args: &[hir::KwArg], kw_args: &[hir::KwArg],
(var_args, kw_var_args): (Option<&hir::PosArg>, Option<&hir::PosArg>),
input: &Input, input: &Input,
namespace: &Context, namespace: &Context,
) -> FailableOption<VarInfo> { ) -> FailableOption<VarInfo> {
@ -2492,7 +2643,15 @@ impl Context {
} }
} }
let found = self let found = self
.search_callee_info(obj, attr_name, pos_args, kw_args, input, namespace) .search_callee_info(
obj,
attr_name,
pos_args,
kw_args,
(var_args, kw_var_args),
input,
namespace,
)
.map_err(|err| (None, TyCheckErrors::from(err)))?; .map_err(|err| (None, TyCheckErrors::from(err)))?;
log!( log!(
"Found:\ncallee: {obj}{}\nfound: {found}", "Found:\ncallee: {obj}{}\nfound: {found}",
@ -2507,7 +2666,15 @@ impl Context {
fmt_slice(kw_args) fmt_slice(kw_args)
); );
let instance = match self let instance = match self
.substitute_call(obj, attr_name, &instance, pos_args, kw_args, namespace) .substitute_call(
obj,
attr_name,
&instance,
pos_args,
kw_args,
(var_args, kw_var_args),
namespace,
)
.map_err(|errs| { .map_err(|errs| {
( (
Some(VarInfo { Some(VarInfo {

View file

@ -290,7 +290,8 @@ impl Args {
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
#[allow(clippy::bool_to_int_with_if)] #[allow(clippy::bool_to_int_with_if)]
let var_argc = if self.var_args.is_none() { 0 } else { 1 }; let var_argc = if self.var_args.is_none() { 0 } else { 1 };
self.pos_args.len() + var_argc + self.kw_args.len() let kw_var_argc = if self.kw_var.is_none() { 0 } else { 1 };
self.pos_args.len() + var_argc + self.kw_args.len() + kw_var_argc
} }
#[inline] #[inline]

View file

@ -1409,6 +1409,7 @@ impl<A: ASTBuildable> GenericASTLowerer<A> {
&call.attr_name, &call.attr_name,
&hir_args.pos_args, &hir_args.pos_args,
&hir_args.kw_args, &hir_args.kw_args,
(hir_args.var_args.as_deref(), hir_args.kw_var.as_deref()),
&self.cfg.input, &self.cfg.input,
&self.module.context, &self.module.context,
) { ) {
@ -1566,6 +1567,7 @@ impl<A: ASTBuildable> GenericASTLowerer<A> {
&Some(attr_name.clone()), &Some(attr_name.clone()),
&args, &args,
&[], &[],
(None, None),
&self.cfg.input, &self.cfg.input,
&self.module.context, &self.module.context,
) { ) {

View file

@ -0,0 +1,8 @@
foo first: Int, second: Int = log first, second
foo(1, 2, *[1, 2]) # ERR
data = {"first": 1, "second": 2}
foo(1, 2, **data) # ERR
foo(1, *2) # ERR
foo(1, **[2]) # ERR

View file

@ -0,0 +1,12 @@
foo first: Int, second: Int, third: Int = log first, second, third
foo(*[1, 2, 3])
foo(1, *[2, 3])
foo(1, 2, *[3])
data = {"first": 1, "second": 2, "third": 3}
foo(**data)
foo(1, **{"second": 2, "third": 3})
# FIXME:
# foo(1, 2, **{"third": 3})
print! "OK"

View file

@ -16,6 +16,11 @@ fn exec_advanced_type_spec() -> Result<(), ()> {
expect_success("tests/should_ok/advanced_type_spec.er", 5) expect_success("tests/should_ok/advanced_type_spec.er", 5)
} }
#[test]
fn exec_args_expansion() -> Result<(), ()> {
expect_success("tests/should_ok/args_expansion.er", 0)
}
#[test] #[test]
fn exec_list_test() -> Result<(), ()> { fn exec_list_test() -> Result<(), ()> {
expect_success("tests/should_ok/list.er", 0) expect_success("tests/should_ok/list.er", 0)
@ -502,6 +507,11 @@ fn exec_args() -> Result<(), ()> {
expect_failure("tests/should_err/args.er", 0, 19) expect_failure("tests/should_err/args.er", 0, 19)
} }
#[test]
fn exec_args_expansion_err() -> Result<(), ()> {
expect_failure("tests/should_err/args_expansion.er", 0, 4)
}
#[test] #[test]
fn exec_list_err() -> Result<(), ()> { fn exec_list_err() -> Result<(), ()> {
expect_failure("examples/list.er", 0, 1) expect_failure("examples/list.er", 0, 1)