From 79931365b72af47a5a56926a56bb6d5968ffbb31 Mon Sep 17 00:00:00 2001 From: ram Date: Wed, 18 Dec 2024 05:10:01 +0000 Subject: [PATCH 1/8] Implement Kwargs support, pending tests and artiq implementation --- nac3artiq/demo/min_artiq.py | 25 ++++++++--- nac3artiq/src/codegen.rs | 42 ++++++++++++++----- nac3core/src/typecheck/type_inferencer/mod.rs | 37 +++++++++++++--- 3 files changed, 83 insertions(+), 21 deletions(-) diff --git a/nac3artiq/demo/min_artiq.py b/nac3artiq/demo/min_artiq.py index 62d32cc3..8d769f05 100644 --- a/nac3artiq/demo/min_artiq.py +++ b/nac3artiq/demo/min_artiq.py @@ -114,13 +114,26 @@ def extern(function): def rpc(arg=None, flags={}): - """Decorates a function or method to be executed on the host interpreter.""" + """Decorates a function to be executed on the host interpreter with kwargs support.""" + def decorator(function): + @wraps(function) + def wrapper(*args, **kwargs): + # Get function signature + sig = inspect.signature(function) + + # Validate kwargs against signature + bound_args = sig.bind(*args, **kwargs) + bound_args.apply_defaults() + + # Call RPC with both args and kwargs + return _do_rpc(function.__name__, + bound_args.args, + bound_args.kwargs) + return wrapper + if arg is None: - def inner_decorator(function): - return rpc(function, flags) - return inner_decorator - register_function(arg) - return arg + return decorator + return decorator(arg) def kernel(function_or_method): """Decorates a function or method to be executed on the core device.""" diff --git a/nac3artiq/src/codegen.rs b/nac3artiq/src/codegen.rs index 653f41a3..e877a055 100644 --- a/nac3artiq/src/codegen.rs +++ b/nac3artiq/src/codegen.rs @@ -79,8 +79,7 @@ pub struct ArtiqCodeGenerator<'a> { /// The [`ParallelMode`] of the current parallel context. /// - /// The current parallel context refers to the nearest `with parallel` or `with legacy_parallel` - /// statement, which is used to determine when and how the timeline should be updated. + /// The current parallel context refers to the nearest `with` statement, which is used to determine when and how the timeline should be updated. parallel_mode: ParallelMode, } @@ -373,8 +372,14 @@ impl<'b> CodeGenerator for ArtiqCodeGenerator<'b> { fn gen_rpc_tag( ctx: &mut CodeGenContext<'_, '_>, ty: Type, + is_kwarg: bool, // Add this parameter buffer: &mut Vec, ) -> Result<(), String> { + // Add kwarg marker if needed + if is_kwarg { + buffer.push(b'k'); // 'k' for keyword argument + } + use nac3core::typecheck::typedef::TypeEnum::*; let int32 = ctx.primitives.int32; @@ -403,14 +408,14 @@ fn gen_rpc_tag( buffer.push(b't'); buffer.push(ty.len() as u8); for ty in ty { - gen_rpc_tag(ctx, *ty, buffer)?; + gen_rpc_tag(ctx, *ty, false, buffer)?; // Pass false for is_kwarg } } TObj { obj_id, params, .. } if *obj_id == PrimDef::List.id() => { let ty = iter_type_vars(params).next().unwrap().ty; buffer.push(b'l'); - gen_rpc_tag(ctx, ty, buffer)?; + gen_rpc_tag(ctx, ty, false, buffer)?; // Pass false for is_kwarg } TObj { obj_id, .. } if *obj_id == PrimDef::NDArray.id() => { let (ndarray_dtype, ndarray_ndims) = unpack_ndarray_var_tys(&mut ctx.unifier, ty); @@ -434,7 +439,7 @@ fn gen_rpc_tag( buffer.push(b'a'); buffer.push((ndarray_ndims & 0xFF) as u8); - gen_rpc_tag(ctx, ndarray_dtype, buffer)?; + gen_rpc_tag(ctx, ndarray_dtype, false, buffer)?; // Pass false for is_kwarg } _ => return Err(format!("Unsupported type: {:?}", ctx.unifier.stringify(ty))), } @@ -808,10 +813,10 @@ fn rpc_codegen_callback_fn<'ctx>( tag.push(b'O'); } for arg in &fun.0.args { - gen_rpc_tag(ctx, arg.ty, &mut tag)?; + gen_rpc_tag(ctx, arg.ty, false, &mut tag)?; // Pass false for is_kwarg } tag.push(b':'); - gen_rpc_tag(ctx, fun.0.ret, &mut tag)?; + gen_rpc_tag(ctx, fun.0.ret, false, &mut tag)?; let mut hasher = DefaultHasher::new(); tag.hash(&mut hasher); @@ -858,8 +863,17 @@ fn rpc_codegen_callback_fn<'ctx>( // -- rpc args handling let mut keys = fun.0.args.clone(); let mut mapping = HashMap::new(); + let mut is_keyword_arg = HashMap::new(); + for (key, value) in args { - mapping.insert(key.unwrap_or_else(|| keys.remove(0).name), value); + if let Some(key_name) = key { + mapping.insert(key_name, value); + is_keyword_arg.insert(key_name, true); + } else { + let arg_name = keys.remove(0).name; + mapping.insert(arg_name, value); + is_keyword_arg.insert(arg_name, false); + } } // default value handling for k in keys { @@ -901,6 +915,14 @@ fn rpc_codegen_callback_fn<'ctx>( ctx.builder.build_store(arg_ptr, arg_slot).unwrap(); } + // Before calling rpc_send/rpc_send_async, add keyword arg info to tag + for arg in &fun.0.args { + if *is_keyword_arg.get(&arg.name).unwrap_or(&false) { + tag.push(b'k'); // Mark as keyword argument + } + gen_rpc_tag(ctx, arg.ty, true, &mut tag)?; // Pass true for is_kwarg + } + // call if is_async { let rpc_send_async = ctx.module.get_function("rpc_send_async").unwrap_or_else(|| { @@ -1007,7 +1029,7 @@ pub fn attributes_writeback<'ctx>( if !is_mutable { continue; } - if gen_rpc_tag(ctx, *field_ty, &mut scratch_buffer).is_ok() { + if gen_rpc_tag(ctx, *field_ty, false, &mut scratch_buffer).is_ok() { attributes.push(name.to_string()); let (index, _) = ctx.get_attr_index(ty, *name); values.push(( @@ -1030,7 +1052,7 @@ pub fn attributes_writeback<'ctx>( TypeEnum::TObj { obj_id, params, .. } if *obj_id == PrimDef::List.id() => { let elem_ty = iter_type_vars(params).next().unwrap().ty; - if gen_rpc_tag(ctx, elem_ty, &mut scratch_buffer).is_ok() { + if gen_rpc_tag(ctx, elem_ty, false, &mut scratch_buffer).is_ok() { let pydict = PyDict::new(py); pydict.set_item("obj", val)?; host_attributes.append(pydict)?; diff --git a/nac3core/src/typecheck/type_inferencer/mod.rs b/nac3core/src/typecheck/type_inferencer/mod.rs index 6068f630..bb579054 100644 --- a/nac3core/src/typecheck/type_inferencer/mod.rs +++ b/nac3core/src/typecheck/type_inferencer/mod.rs @@ -1832,20 +1832,47 @@ impl<'a> Inferencer<'a> { if let TypeEnum::TFunc(sign) = &*self.unifier.get_ty(func.custom.unwrap()) { if sign.vars.is_empty() { + // Build keyword argument map + let mut kwargs_map = HashMap::new(); + for kw in &keywords { + if let Some(name) = &kw.node.arg { + // Check if keyword arg exists in function signature + if !sign.args.iter().any(|arg| arg.name == *name) { + return report_error( + &format!("Unexpected keyword argument '{}'", name), + kw.location, + ); + } + kwargs_map.insert(*name, kw.node.value.custom.unwrap()); + } + } + + // Validate that all required args are provided + for arg in &sign.args { + if arg.default_value.is_none() + && !kwargs_map.contains_key(&arg.name) + && args.len() < sign.args.len() + { + return report_error( + &format!("Missing required argument '{}'", arg.name), + location, + ); + } + } + let call = Call { posargs: args.iter().map(|v| v.custom.unwrap()).collect(), - kwargs: keywords - .iter() - .map(|v| (*v.node.arg.as_ref().unwrap(), v.node.value.custom.unwrap())) - .collect(), + kwargs: kwargs_map, fun: RefCell::new(None), ret: sign.ret, loc: Some(location), operator_info: None, }; + self.unifier.unify_call(&call, func.custom.unwrap(), sign).map_err(|e| { HashSet::from([e.at(Some(location)).to_display(self.unifier).to_string()]) })?; + return Ok(Located { location, custom: Some(sign.ret), @@ -1859,7 +1886,7 @@ impl<'a> Inferencer<'a> { posargs: args.iter().map(|v| v.custom.unwrap()).collect(), kwargs: keywords .iter() - .map(|v| (*v.node.arg.as_ref().unwrap(), v.custom.unwrap())) + .filter_map(|v| v.node.arg.map(|name| (name, v.node.value.custom.unwrap()))) .collect(), fun: RefCell::new(None), ret, -- 2.47.2 From ec2787aaf66eea425283b1fbd8430d95283d2815 Mon Sep 17 00:00:00 2001 From: ram Date: Wed, 5 Feb 2025 07:19:27 +0000 Subject: [PATCH 2/8] Implement passing of kwarg --- nac3artiq/demo/min_artiq.py | 25 +++++-------------- nac3artiq/demo/rpc_kwargs_test.py | 41 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 nac3artiq/demo/rpc_kwargs_test.py diff --git a/nac3artiq/demo/min_artiq.py b/nac3artiq/demo/min_artiq.py index 8d769f05..62d32cc3 100644 --- a/nac3artiq/demo/min_artiq.py +++ b/nac3artiq/demo/min_artiq.py @@ -114,26 +114,13 @@ def extern(function): def rpc(arg=None, flags={}): - """Decorates a function to be executed on the host interpreter with kwargs support.""" - def decorator(function): - @wraps(function) - def wrapper(*args, **kwargs): - # Get function signature - sig = inspect.signature(function) - - # Validate kwargs against signature - bound_args = sig.bind(*args, **kwargs) - bound_args.apply_defaults() - - # Call RPC with both args and kwargs - return _do_rpc(function.__name__, - bound_args.args, - bound_args.kwargs) - return wrapper - + """Decorates a function or method to be executed on the host interpreter.""" if arg is None: - return decorator - return decorator(arg) + def inner_decorator(function): + return rpc(function, flags) + return inner_decorator + register_function(arg) + return arg def kernel(function_or_method): """Decorates a function or method to be executed on the core device.""" diff --git a/nac3artiq/demo/rpc_kwargs_test.py b/nac3artiq/demo/rpc_kwargs_test.py new file mode 100644 index 00000000..acc86bba --- /dev/null +++ b/nac3artiq/demo/rpc_kwargs_test.py @@ -0,0 +1,41 @@ +from min_artiq import * +from numpy import int32 + +@rpc +def sum_3(a: int32, b: int32 = 10, c: int32 = 20) -> int32: + """ + An RPC function to test NAC3's handling of positional/keyword arguments. + """ + return int32(a + b + c) + +@nac3 +class RpcKwargTest: + core: KernelInvariant[Core] + + def __init__(self): + self.core = Core() + + @kernel + def run(self): + #1) All positional => a=1, b=2, c=3 -> total=6 + s1 = sum_3(1, 2, 3) + if s1 != 6: + raise ValueError("sum_3(1,2,3) gave the wrong result.") + + #2) Use the default b=10, c=20 => a=5 => total=35 + s2 = sum_3(5) + if s2 != 35: + raise ValueError("sum_3(5) gave the wrong result.") + + #3) a=1 (positional), b=100 (keyword), omit c => c=20 => total=121 + s3 = sum_3(1, b=100) + if s3 != 121: + raise ValueError("sum_3(1, b=100) gave the wrong result.") + + #4) a=2, c=300 => b=10 (default) => total=312 + s4 = sum_3(a=2, c=300) + if s4 != 312: + raise ValueError("sum_3(a=2, c=300) gave the wrong result.") + +if __name__ == "__main__": + RpcKwargTest().run() \ No newline at end of file -- 2.47.2 From c083245c280f3d28fc286dfb03417f3d7ce521d6 Mon Sep 17 00:00:00 2001 From: ram Date: Wed, 5 Feb 2025 07:20:13 +0000 Subject: [PATCH 3/8] Implement passing of kwarg --- nac3artiq/src/codegen.rs | 251 +++++++++--------- nac3core/src/typecheck/type_inferencer/mod.rs | 84 +++--- 2 files changed, 174 insertions(+), 161 deletions(-) diff --git a/nac3artiq/src/codegen.rs b/nac3artiq/src/codegen.rs index e877a055..26916279 100644 --- a/nac3artiq/src/codegen.rs +++ b/nac3artiq/src/codegen.rs @@ -792,11 +792,11 @@ fn format_rpc_ret<'ctx>( Some(result) } -fn rpc_codegen_callback_fn<'ctx>( +pub fn rpc_codegen_callback_fn<'ctx>( ctx: &mut CodeGenContext<'ctx, '_>, obj: Option<(Type, ValueEnum<'ctx>)>, fun: (&FunSignature, DefinitionId), - args: Vec<(Option, ValueEnum<'ctx>)>, + mut args: Vec<(Option, ValueEnum<'ctx>)>, generator: &mut dyn CodeGenerator, is_async: bool, ) -> Result>, String> { @@ -804,136 +804,140 @@ fn rpc_codegen_callback_fn<'ctx>( let int32 = ctx.ctx.i32_type(); let size_type = generator.get_size_type(ctx.ctx); let ptr_type = int8.ptr_type(AddressSpace::default()); - let tag_ptr_type = ctx.ctx.struct_type(&[ptr_type.into(), size_type.into()], false); - + let tag_ptr_type = ctx + .ctx + .struct_type(&[ptr_type.into(), size_type.into()], false) + .ptr_type(AddressSpace::default()); let service_id = int32.const_int(fun.1 .0 as u64, false); // -- setup rpc tags let mut tag = Vec::new(); if obj.is_some() { tag.push(b'O'); } - for arg in &fun.0.args { - gen_rpc_tag(ctx, arg.ty, false, &mut tag)?; // Pass false for is_kwarg + for param in &fun.0.args { + gen_rpc_tag(ctx, param.ty, false, &mut tag)?; } tag.push(b':'); gen_rpc_tag(ctx, fun.0.ret, false, &mut tag)?; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); tag.hash(&mut hasher); - let hash = format!("{}", hasher.finish()); + let hash = format!("rpc_tag_{}", hasher.finish()); - let tag_ptr = ctx - .module - .get_global(hash.as_str()) - .unwrap_or_else(|| { - let tag_arr_ptr = ctx.module.add_global( - int8.array_type(tag.len() as u32), - None, - format!("tagptr{}", fun.1 .0).as_str(), - ); - tag_arr_ptr.set_initializer(&int8.const_array( - &tag.iter().map(|v| int8.const_int(u64::from(*v), false)).collect::>(), - )); - tag_arr_ptr.set_linkage(Linkage::Private); - let tag_ptr = ctx.module.add_global(tag_ptr_type, None, &hash); - tag_ptr.set_linkage(Linkage::Private); - tag_ptr.set_initializer(&ctx.ctx.const_struct( - &[ - tag_arr_ptr.as_pointer_value().const_cast(ptr_type).into(), - size_type.const_int(tag.len() as u64, false).into(), - ], - false, - )); - tag_ptr - }) - .as_pointer_value(); + let maybe_existing = ctx.module.get_global(&hash); + let tag_ptr = if let Some(gv) = maybe_existing { + gv.as_pointer_value() + } else { + let tag_len = tag.len(); + let arr_ty = int8.array_type(tag_len as u32); + let tag_const = int8.const_array( + &tag.iter().map(|&b| int8.const_int(b as u64, false)).collect::>(), + ); + let arr_gv = ctx + .module + .add_global(arr_ty, None, &format!("{}.arr", hash)); + arr_gv.set_linkage(Linkage::Private); + arr_gv.set_initializer(&tag_const); - let arg_length = args.len() + usize::from(obj.is_some()); + let st = ctx.ctx.const_struct( + &[ + arr_gv.as_pointer_value().const_cast(ptr_type).into(), + size_type.const_int(tag_len as u64, false).into(), + ], + false, + ); + let st_gv = ctx.module.add_global(st.get_type(), None, &hash); + st_gv.set_linkage(Linkage::Private); + st_gv.set_initializer(&st); + st_gv.as_pointer_value() + }; + + let n_params = fun.0.args.len(); + let mut param_map: Vec>> = vec![None; n_params]; + + let mut pos_index = 0usize; + + if let Some((obj_ty, obj_val)) = obj { + param_map[0] = Some(obj_val); + pos_index = 1; + } + for (maybe_key, val_enum) in args.drain(..) { + if let Some(kw_name) = maybe_key { + let param_pos = fun.0 + .args + .iter() + .position(|arg| arg.name == kw_name) + .ok_or_else(|| format!("Unknown keyword argument '{}'", kw_name))?; + + if param_map[param_pos].is_some() { + return Err(format!("Multiple values for argument '{}'", kw_name)); + } + param_map[param_pos] = Some(val_enum); + } else { + while pos_index < n_params && param_map[pos_index].is_some() { + pos_index += 1; + } + if pos_index >= n_params { + return Err("Too many positional arguments given to function.".to_string()); + } + param_map[pos_index] = Some(val_enum); + pos_index += 1; + } + } + + for (i, param) in fun.0.args.iter().enumerate() { + if param_map[i].is_none() { + if let Some(default_expr) = ¶m.default_value { + let default_val = ctx.gen_symbol_val(generator, default_expr, param.ty).into(); + param_map[i] = Some(default_val); + } else { + return Err(format!("Missing required argument '{}'", param.name)); + } + } + } + let mut real_params = Vec::with_capacity(n_params); + for (i, param_spec) in fun.0.args.iter().enumerate() { + let some_valenum = param_map[i].take().unwrap(); + let llvm_val = some_valenum.to_basic_value_enum(ctx, generator, param_spec.ty)?; + real_params.push((llvm_val, param_spec.ty)); + } + + let arg_count = real_params.len() as u64; let stackptr = call_stacksave(ctx, Some("rpc.stack")); - let args_ptr = ctx + + let i32_ty = ctx.ctx.i32_type(); + let arg_array = ctx .builder .build_array_alloca( ptr_type, - ctx.ctx.i32_type().const_int(arg_length as u64, false), - "argptr", + i32_ty.const_int(arg_count, false), + "rpc.arg_array", ) .unwrap(); - // -- rpc args handling - let mut keys = fun.0.args.clone(); - let mut mapping = HashMap::new(); - let mut is_keyword_arg = HashMap::new(); - - for (key, value) in args { - if let Some(key_name) = key { - mapping.insert(key_name, value); - is_keyword_arg.insert(key_name, true); - } else { - let arg_name = keys.remove(0).name; - mapping.insert(arg_name, value); - is_keyword_arg.insert(arg_name, false); - } - } - // default value handling - for k in keys { - mapping - .insert(k.name, ctx.gen_symbol_val(generator, &k.default_value.unwrap(), k.ty).into()); - } - // reorder the parameters - let mut real_params = fun - .0 - .args - .iter() - .map(|arg| { - mapping - .remove(&arg.name) - .unwrap() - .to_basic_value_enum(ctx, generator, arg.ty) - .map(|llvm_val| (llvm_val, arg.ty)) - }) - .collect::, _>>()?; - if let Some(obj) = obj { - if let ValueEnum::Static(obj_val) = obj.1 { - real_params.insert(0, (obj_val.get_const_obj(ctx, generator), obj.0)); - } else { - // should be an error here... - panic!("only host object is allowed"); - } - } - - for (i, (arg, arg_ty)) in real_params.iter().enumerate() { - let arg_slot = format_rpc_arg(generator, ctx, (*arg, *arg_ty, i)); - let arg_ptr = unsafe { + for (i, (llvm_val, ty)) in real_params.iter().enumerate() { + let arg_slot_ptr = unsafe { ctx.builder.build_gep( - args_ptr, - &[int32.const_int(i as u64, false)], - &format!("rpc.arg{i}"), + arg_array, + &[ + i32_ty.const_int(i as u64, false), + ], + &format!("rpc.arg_slot_{}", i), ) - } - .unwrap(); - ctx.builder.build_store(arg_ptr, arg_slot).unwrap(); + }.unwrap(); + let arg_ptr = format_rpc_arg(generator, ctx, (*llvm_val, *ty, i)); + ctx.builder.build_store(arg_slot_ptr, arg_ptr).unwrap(); } - // Before calling rpc_send/rpc_send_async, add keyword arg info to tag - for arg in &fun.0.args { - if *is_keyword_arg.get(&arg.name).unwrap_or(&false) { - tag.push(b'k'); // Mark as keyword argument - } - gen_rpc_tag(ctx, arg.ty, true, &mut tag)?; // Pass true for is_kwarg - } - - // call if is_async { let rpc_send_async = ctx.module.get_function("rpc_send_async").unwrap_or_else(|| { ctx.module.add_function( "rpc_send_async", ctx.ctx.void_type().fn_type( - &[ - int32.into(), - tag_ptr_type.ptr_type(AddressSpace::default()).into(), - ptr_type.ptr_type(AddressSpace::default()).into(), - ], + &[int32.into(), tag_ptr_type.into(), ptr_type.ptr_type(AddressSpace::default()).into()], false, ), None, @@ -942,45 +946,42 @@ fn rpc_codegen_callback_fn<'ctx>( ctx.builder .build_call( rpc_send_async, - &[service_id.into(), tag_ptr.into(), args_ptr.into()], - "rpc.send", + &[ + service_id.into(), + tag_ptr.into(), + arg_array.into(), + ], + "rpc.send_async", ) .unwrap(); + call_stackrestore(ctx, stackptr); + return Ok(None); } else { let rpc_send = ctx.module.get_function("rpc_send").unwrap_or_else(|| { ctx.module.add_function( "rpc_send", ctx.ctx.void_type().fn_type( - &[ - int32.into(), - tag_ptr_type.ptr_type(AddressSpace::default()).into(), - ptr_type.ptr_type(AddressSpace::default()).into(), - ], + &[int32.into(), tag_ptr_type.into(), ptr_type.ptr_type(AddressSpace::default()).into()], false, ), None, ) }); ctx.builder - .build_call(rpc_send, &[service_id.into(), tag_ptr.into(), args_ptr.into()], "rpc.send") + .build_call( + rpc_send, + &[ + service_id.into(), + tag_ptr.into(), + arg_array.into(), + ], + "rpc.send", + ) .unwrap(); - } + call_stackrestore(ctx, stackptr); - // reclaim stack space used by arguments - call_stackrestore(ctx, stackptr); - - if is_async { - // async RPCs do not return any values - Ok(None) - } else { - let result = format_rpc_ret(generator, ctx, fun.0.ret); - - if !result.is_some_and(|res| res.get_type().is_pointer_type()) { - // An RPC returning an NDArray would not touch here. - call_stackrestore(ctx, stackptr); - } - - Ok(result) + let maybe_ret = format_rpc_ret(generator, ctx, fun.0.ret); + Ok(maybe_ret) } } diff --git a/nac3core/src/typecheck/type_inferencer/mod.rs b/nac3core/src/typecheck/type_inferencer/mod.rs index bb579054..742fa197 100644 --- a/nac3core/src/typecheck/type_inferencer/mod.rs +++ b/nac3core/src/typecheck/type_inferencer/mod.rs @@ -3,7 +3,7 @@ use std::{ cmp::max, collections::{HashMap, HashSet}, convert::{From, TryInto}, - iter::once, + iter::{once, repeat_n}, sync::Arc, }; @@ -187,7 +187,7 @@ fn fix_assignment_target_context(node: &mut ast::Located) { } } -impl<'a> Fold<()> for Inferencer<'a> { +impl Fold<()> for Inferencer<'_> { type TargetU = Option; type Error = InferenceError; @@ -657,7 +657,7 @@ impl<'a> Fold<()> for Inferencer<'a> { type InferenceResult = Result; -impl<'a> Inferencer<'a> { +impl Inferencer<'_> { /// Constrain a <: b /// Currently implemented as unification fn constrain(&mut self, a: Type, b: Type, location: &Location) -> Result<(), InferenceError> { @@ -1234,6 +1234,45 @@ impl<'a> Inferencer<'a> { })); } + if ["np_shape".into(), "np_strides".into()].contains(id) && args.len() == 1 { + let ndarray = self.fold_expr(args.remove(0))?; + + let ndims = arraylike_get_ndims(self.unifier, ndarray.custom.unwrap()); + + // Make a tuple of size `ndims` full of int32 (TODO: Make it usize) + let ret_ty = TypeEnum::TTuple { + ty: repeat_n(self.primitives.int32, ndims as usize).collect_vec(), + is_vararg_ctx: false, + }; + let ret_ty = self.unifier.add_ty(ret_ty); + + let func_ty = TypeEnum::TFunc(FunSignature { + args: vec![FuncArg { + name: "a".into(), + default_value: None, + ty: ndarray.custom.unwrap(), + is_vararg: false, + }], + ret: ret_ty, + vars: VarMap::new(), + }); + let func_ty = self.unifier.add_ty(func_ty); + + return Ok(Some(Located { + location, + custom: Some(ret_ty), + node: ExprKind::Call { + func: Box::new(Located { + custom: Some(func_ty), + location: func.location, + node: ExprKind::Name { id: *id, ctx: *ctx }, + }), + args: vec![ndarray], + keywords: vec![], + }, + })); + } + if id == &"np_dot".into() { let arg0 = self.fold_expr(args.remove(0))?; let arg1 = self.fold_expr(args.remove(0))?; @@ -1555,7 +1594,7 @@ impl<'a> Inferencer<'a> { })); } // 2-argument ndarray n-dimensional factory functions - if id == &"np_reshape".into() && args.len() == 2 { + if ["np_reshape".into(), "np_broadcast_to".into()].contains(id) && args.len() == 2 { let arg0 = self.fold_expr(args.remove(0))?; let shape_expr = args.remove(0); @@ -1832,47 +1871,20 @@ impl<'a> Inferencer<'a> { if let TypeEnum::TFunc(sign) = &*self.unifier.get_ty(func.custom.unwrap()) { if sign.vars.is_empty() { - // Build keyword argument map - let mut kwargs_map = HashMap::new(); - for kw in &keywords { - if let Some(name) = &kw.node.arg { - // Check if keyword arg exists in function signature - if !sign.args.iter().any(|arg| arg.name == *name) { - return report_error( - &format!("Unexpected keyword argument '{}'", name), - kw.location, - ); - } - kwargs_map.insert(*name, kw.node.value.custom.unwrap()); - } - } - - // Validate that all required args are provided - for arg in &sign.args { - if arg.default_value.is_none() - && !kwargs_map.contains_key(&arg.name) - && args.len() < sign.args.len() - { - return report_error( - &format!("Missing required argument '{}'", arg.name), - location, - ); - } - } - let call = Call { posargs: args.iter().map(|v| v.custom.unwrap()).collect(), - kwargs: kwargs_map, + kwargs: keywords + .iter() + .map(|v| (*v.node.arg.as_ref().unwrap(), v.node.value.custom.unwrap())) + .collect(), fun: RefCell::new(None), ret: sign.ret, loc: Some(location), operator_info: None, }; - self.unifier.unify_call(&call, func.custom.unwrap(), sign).map_err(|e| { HashSet::from([e.at(Some(location)).to_display(self.unifier).to_string()]) })?; - return Ok(Located { location, custom: Some(sign.ret), @@ -1886,7 +1898,7 @@ impl<'a> Inferencer<'a> { posargs: args.iter().map(|v| v.custom.unwrap()).collect(), kwargs: keywords .iter() - .filter_map(|v| v.node.arg.map(|name| (name, v.node.value.custom.unwrap()))) + .map(|v| (*v.node.arg.as_ref().unwrap(), v.custom.unwrap())) .collect(), fun: RefCell::new(None), ret, -- 2.47.2 From c5aa042287e70c4caaa95615d7abeae01210b464 Mon Sep 17 00:00:00 2001 From: ram Date: Sun, 9 Feb 2025 16:42:52 +0000 Subject: [PATCH 4/8] [WIP] Update based on feedback --- nac3artiq/demo/rpc_kwargs_test.py | 12 ++---- nac3artiq/src/codegen.rs | 71 ++++++++++++++----------------- 2 files changed, 35 insertions(+), 48 deletions(-) diff --git a/nac3artiq/demo/rpc_kwargs_test.py b/nac3artiq/demo/rpc_kwargs_test.py index acc86bba..d4a444b2 100644 --- a/nac3artiq/demo/rpc_kwargs_test.py +++ b/nac3artiq/demo/rpc_kwargs_test.py @@ -19,23 +19,19 @@ class RpcKwargTest: def run(self): #1) All positional => a=1, b=2, c=3 -> total=6 s1 = sum_3(1, 2, 3) - if s1 != 6: - raise ValueError("sum_3(1,2,3) gave the wrong result.") + assert s1 == 6 #2) Use the default b=10, c=20 => a=5 => total=35 s2 = sum_3(5) - if s2 != 35: - raise ValueError("sum_3(5) gave the wrong result.") + assert s2 == 35 #3) a=1 (positional), b=100 (keyword), omit c => c=20 => total=121 s3 = sum_3(1, b=100) - if s3 != 121: - raise ValueError("sum_3(1, b=100) gave the wrong result.") + assert s3 == 121 #4) a=2, c=300 => b=10 (default) => total=312 s4 = sum_3(a=2, c=300) - if s4 != 312: - raise ValueError("sum_3(a=2, c=300) gave the wrong result.") + assert s4 == 312 if __name__ == "__main__": RpcKwargTest().run() \ No newline at end of file diff --git a/nac3artiq/src/codegen.rs b/nac3artiq/src/codegen.rs index 26916279..9f0211e6 100644 --- a/nac3artiq/src/codegen.rs +++ b/nac3artiq/src/codegen.rs @@ -1,5 +1,5 @@ use std::{ - collections::{hash_map::DefaultHasher, HashMap}, + collections::hash_map::DefaultHasher, hash::{Hash, Hasher}, iter::once, mem, @@ -79,7 +79,8 @@ pub struct ArtiqCodeGenerator<'a> { /// The [`ParallelMode`] of the current parallel context. /// - /// The current parallel context refers to the nearest `with` statement, which is used to determine when and how the timeline should be updated. + /// The current parallel context refers to the nearest `with` statement, + /// which is used to determine when and how the timeline should be updated. parallel_mode: ParallelMode, } @@ -372,12 +373,11 @@ impl<'b> CodeGenerator for ArtiqCodeGenerator<'b> { fn gen_rpc_tag( ctx: &mut CodeGenContext<'_, '_>, ty: Type, - is_kwarg: bool, // Add this parameter + is_kwarg: bool, buffer: &mut Vec, ) -> Result<(), String> { - // Add kwarg marker if needed if is_kwarg { - buffer.push(b'k'); // 'k' for keyword argument + buffer.push(b'k'); } use nac3core::typecheck::typedef::TypeEnum::*; @@ -408,14 +408,14 @@ fn gen_rpc_tag( buffer.push(b't'); buffer.push(ty.len() as u8); for ty in ty { - gen_rpc_tag(ctx, *ty, false, buffer)?; // Pass false for is_kwarg + gen_rpc_tag(ctx, *ty, false, buffer)?; } } TObj { obj_id, params, .. } if *obj_id == PrimDef::List.id() => { let ty = iter_type_vars(params).next().unwrap().ty; buffer.push(b'l'); - gen_rpc_tag(ctx, ty, false, buffer)?; // Pass false for is_kwarg + gen_rpc_tag(ctx, ty, false, buffer)?; } TObj { obj_id, .. } if *obj_id == PrimDef::NDArray.id() => { let (ndarray_dtype, ndarray_ndims) = unpack_ndarray_var_tys(&mut ctx.unifier, ty); @@ -796,7 +796,7 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx: &mut CodeGenContext<'ctx, '_>, obj: Option<(Type, ValueEnum<'ctx>)>, fun: (&FunSignature, DefinitionId), - mut args: Vec<(Option, ValueEnum<'ctx>)>, + args: Vec<(Option, ValueEnum<'ctx>)>, generator: &mut dyn CodeGenerator, is_async: bool, ) -> Result>, String> { @@ -820,8 +820,6 @@ pub fn rpc_codegen_callback_fn<'ctx>( tag.push(b':'); gen_rpc_tag(ctx, fun.0.ret, false, &mut tag)?; - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); tag.hash(&mut hasher); let hash = format!("rpc_tag_{}", hasher.finish()); @@ -832,12 +830,9 @@ pub fn rpc_codegen_callback_fn<'ctx>( } else { let tag_len = tag.len(); let arr_ty = int8.array_type(tag_len as u32); - let tag_const = int8.const_array( - &tag.iter().map(|&b| int8.const_int(b as u64, false)).collect::>(), - ); - let arr_gv = ctx - .module - .add_global(arr_ty, None, &format!("{}.arr", hash)); + let tag_const = int8 + .const_array(&tag.iter().map(|&b| int8.const_int(b as u64, false)).collect::>()); + let arr_gv = ctx.module.add_global(arr_ty, None, &format!("{}.arr", hash)); arr_gv.set_linkage(Linkage::Private); arr_gv.set_initializer(&tag_const); @@ -860,13 +855,14 @@ pub fn rpc_codegen_callback_fn<'ctx>( let mut pos_index = 0usize; - if let Some((obj_ty, obj_val)) = obj { + if let Some((_obj_ty, obj_val)) = obj { param_map[0] = Some(obj_val); pos_index = 1; } - for (maybe_key, val_enum) in args.drain(..) { + for (maybe_key, val_enum) in args { if let Some(kw_name) = maybe_key { - let param_pos = fun.0 + let param_pos = fun + .0 .args .iter() .position(|arg| arg.name == kw_name) @@ -911,23 +907,18 @@ pub fn rpc_codegen_callback_fn<'ctx>( let i32_ty = ctx.ctx.i32_type(); let arg_array = ctx .builder - .build_array_alloca( - ptr_type, - i32_ty.const_int(arg_count, false), - "rpc.arg_array", - ) + .build_array_alloca(ptr_type, i32_ty.const_int(arg_count, false), "rpc.arg_array") .unwrap(); for (i, (llvm_val, ty)) in real_params.iter().enumerate() { let arg_slot_ptr = unsafe { ctx.builder.build_gep( arg_array, - &[ - i32_ty.const_int(i as u64, false), - ], + &[i32_ty.const_int(i as u64, false)], &format!("rpc.arg_slot_{}", i), ) - }.unwrap(); + } + .unwrap(); let arg_ptr = format_rpc_arg(generator, ctx, (*llvm_val, *ty, i)); ctx.builder.build_store(arg_slot_ptr, arg_ptr).unwrap(); } @@ -937,7 +928,11 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx.module.add_function( "rpc_send_async", ctx.ctx.void_type().fn_type( - &[int32.into(), tag_ptr_type.into(), ptr_type.ptr_type(AddressSpace::default()).into()], + &[ + int32.into(), + tag_ptr_type.into(), + ptr_type.ptr_type(AddressSpace::default()).into(), + ], false, ), None, @@ -946,11 +941,7 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx.builder .build_call( rpc_send_async, - &[ - service_id.into(), - tag_ptr.into(), - arg_array.into(), - ], + &[service_id.into(), tag_ptr.into(), arg_array.into()], "rpc.send_async", ) .unwrap(); @@ -961,7 +952,11 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx.module.add_function( "rpc_send", ctx.ctx.void_type().fn_type( - &[int32.into(), tag_ptr_type.into(), ptr_type.ptr_type(AddressSpace::default()).into()], + &[ + int32.into(), + tag_ptr_type.into(), + ptr_type.ptr_type(AddressSpace::default()).into(), + ], false, ), None, @@ -970,11 +965,7 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx.builder .build_call( rpc_send, - &[ - service_id.into(), - tag_ptr.into(), - arg_array.into(), - ], + &[service_id.into(), tag_ptr.into(), arg_array.into()], "rpc.send", ) .unwrap(); -- 2.47.2 From 0aa9847a50e31aaf568fe6ec508fd232e9f57bdb Mon Sep 17 00:00:00 2001 From: ram Date: Mon, 10 Feb 2025 03:00:11 +0000 Subject: [PATCH 5/8] Final changes based on feedback and running cargo clippy --tests --- nac3artiq/src/codegen.rs | 62 +++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/nac3artiq/src/codegen.rs b/nac3artiq/src/codegen.rs index d05158bd..c2c8d8bc 100644 --- a/nac3artiq/src/codegen.rs +++ b/nac3artiq/src/codegen.rs @@ -842,9 +842,10 @@ pub fn rpc_codegen_callback_fn<'ctx>( } else { let tag_len = tag.len(); let arr_ty = int8.array_type(tag_len as u32); - let tag_const = int8 - .const_array(&tag.iter().map(|&b| int8.const_int(b as u64, false)).collect::>()); - let arr_gv = ctx.module.add_global(arr_ty, None, &format!("{}.arr", hash)); + let tag_const = int8.const_array( + &tag.iter().map(|&b| int8.const_int(u64::from(b), false)).collect::>(), + ); + let arr_gv = ctx.module.add_global(arr_ty, None, &format!("{hash}.arr")); arr_gv.set_linkage(Linkage::Private); arr_gv.set_initializer(&tag_const); @@ -878,10 +879,10 @@ pub fn rpc_codegen_callback_fn<'ctx>( .args .iter() .position(|arg| arg.name == kw_name) - .ok_or_else(|| format!("Unknown keyword argument '{}'", kw_name))?; + .ok_or_else(|| format!("Unknown keyword argument '{kw_name}'"))?; if param_map[param_pos].is_some() { - return Err(format!("Multiple values for argument '{}'", kw_name)); + return Err(format!("Multiple values for argument '{kw_name}'")); } param_map[param_pos] = Some(val_enum); } else { @@ -927,7 +928,7 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx.builder.build_gep( arg_array, &[i32_ty.const_int(i as u64, false)], - &format!("rpc.arg_slot_{}", i), + &format!("rpc.arg_slot_{i}"), ) } .unwrap(); @@ -959,33 +960,28 @@ pub fn rpc_codegen_callback_fn<'ctx>( .unwrap(); call_stackrestore(ctx, stackptr); return Ok(None); - } else { - let rpc_send = ctx.module.get_function("rpc_send").unwrap_or_else(|| { - ctx.module.add_function( - "rpc_send", - ctx.ctx.void_type().fn_type( - &[ - int32.into(), - tag_ptr_type.into(), - ptr_type.ptr_type(AddressSpace::default()).into(), - ], - false, - ), - None, - ) - }); - ctx.builder - .build_call( - rpc_send, - &[service_id.into(), tag_ptr.into(), arg_array.into()], - "rpc.send", - ) - .unwrap(); - call_stackrestore(ctx, stackptr); - - let maybe_ret = format_rpc_ret(generator, ctx, fun.0.ret); - Ok(maybe_ret) } + let rpc_send = ctx.module.get_function("rpc_send").unwrap_or_else(|| { + ctx.module.add_function( + "rpc_send", + ctx.ctx.void_type().fn_type( + &[ + int32.into(), + tag_ptr_type.into(), + ptr_type.ptr_type(AddressSpace::default()).into(), + ], + false, + ), + None, + ) + }); + ctx.builder + .build_call(rpc_send, &[service_id.into(), tag_ptr.into(), arg_array.into()], "rpc.send") + .unwrap(); + call_stackrestore(ctx, stackptr); + + let maybe_ret = format_rpc_ret(generator, ctx, fun.0.ret); + Ok(maybe_ret) } pub fn attributes_writeback<'ctx>( @@ -1074,7 +1070,7 @@ pub fn attributes_writeback<'ctx>( if *is_method { continue; } - if gen_rpc_tag(ctx, *field_ty, &mut scratch_buffer).is_ok() { + if gen_rpc_tag(ctx, *field_ty, false, &mut scratch_buffer).is_ok() { fields.push(name.to_string()); let (index, _) = ctx.get_attr_index(ty, *name); values.push(( -- 2.47.2 From 1ad9c3672d501134e117326beefb39856cc951fb Mon Sep 17 00:00:00 2001 From: ram Date: Mon, 10 Feb 2025 04:58:27 +0000 Subject: [PATCH 6/8] Implemented passing or kwargs into RPC --- nac3artiq/demo/module.elf | Bin 0 -> 2940 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 nac3artiq/demo/module.elf diff --git a/nac3artiq/demo/module.elf b/nac3artiq/demo/module.elf new file mode 100644 index 0000000000000000000000000000000000000000..1516a2479a74f16952fe19edb313ea76905e57df GIT binary patch literal 2940 zcmb_ePiP!f7=N=nv)R}*u9=2LBeG=DvV?V#S`Sith_V&YATcPFB1|T;+w9WpChknM z5y6BesJE4+^x(k=5j=WmZXQ->pa+i%9y}DRc=2X6$r^V3{buG(CoN(Q{ouFn`}3Rc zee>Qo@9oDIre0ALMMz1CKLxEHf#-qIULjOp&k9ZC#N(phkHZxSxdeG4BckV}ztAHD zp~d1uX(1#Q;3raHJc02%;u5dICK%@&)4zGe?~qL5Y4`}n2whRS#_1=-4d}nGwh}kM zZEozidTLGR&;!2^*RtP}tIV(Z&+Ouvv@yDtHby>A>#EY*R2A?u!pLoPbWQCV`K`Zo zEh~)Ev~P6`llFu#Mrp5ijO^B4Fqc--wMMU=THlp(>1!%-6(ZY7XbFAr6nuXPeOc?C z)xMh46)mdm*8#1QJXP3jYO}%_+I}Xe1@)w~uCAm+Q=1mX$jx?BE3}Q=cBiRr3L{Hf zZ5v~>Cl$lIx!2Tg?Hl>+PEglIk#)suLf)=l+lCs=eWcpHR#C%5!pUqu^>A%ivpnZs z-w$e=Oc`Ue3;R)RIofKhww+6lwzftO*6qZ@wQ>Lab=iy7ZPc%IFQ~0GfOTc9d-vB( zd-KuOwyI;@GPq76*a{ZJ#oGY6!#`vxc^joxEJ@_u+`&PmCc!2M+E&ILOO&-UQ;Gb(8 zDbja08QA{sc=g!@d(IuN+0I%2$F6&o_r{B*>f(5P$+50{VAtne%PYB_xwIlIt7=zj zmSxT?Ij=MH76bL=nmHkc!nDeV(^E4-`AF&cVh88j@Ni$kk7W^`{-@jJS*u#zZnw~% zDXb}i{_{W?G1jJD^kWTWYEAhx@maFamln!!^2JTKd0q~I*MdHU@fVEu;L~}KLuN7R zBte#_hws{m>GxtBlooOFF}*=O%=1mme>ES2i+V$EKtc>WG87Av)i zXITi(xvu4U*i&I4sjyra&J}BZ*{&DE8UJ&@GDU)`j@Mlz)Sry3AU5)VJ^q0GGHl+H zlmXnFZ(v-8ev&@+{AuTPO*$zF4$9+jPD98S{)VOJ(bF-L4`aHLF~#gH~Fw&EiT;@CW2z zT&W7PY`bM)y37J6a}_Rve%TB=Cf+1`*sfL_VWL6sP#Kv}M^xqiD(nq{>mdL8426Bp zho#E(Adl<%2b@F>x)jM{FOl~ga02T`Oi5B0W1P>*e6!#X@5XpCAM;3F{J)c=O5P+K z)L9?*jo`j<|0s-c|6T*wN91w;3GTZT&PAC4*bC&bKZs4}avkJ%#9qf#KCgu!@eb_x zyqtR$U~YMC@a)LKmLmD{aQHk-%=3hh??|rHKY`9YW4`!*PE79$dCb`eNAmbh%I^}U TFEZbCSma0{?@MW5l@--QPk literal 0 HcmV?d00001 -- 2.47.2 From f63a2c449817b4f4f14b7af0b0d719b4f262b174 Mon Sep 17 00:00:00 2001 From: ram Date: Mon, 10 Feb 2025 07:21:52 +0000 Subject: [PATCH 7/8] Update based on feedback --- nac3artiq/src/codegen.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/nac3artiq/src/codegen.rs b/nac3artiq/src/codegen.rs index 13bba161..dacfda9a 100644 --- a/nac3artiq/src/codegen.rs +++ b/nac3artiq/src/codegen.rs @@ -41,7 +41,9 @@ use nac3core::{ numpy::unpack_ndarray_var_tys, DefinitionId, GenCall, }, - typecheck::typedef::{iter_type_vars, FunSignature, FuncArg, Type, TypeEnum, VarMap}, + typecheck::typedef::{ + iter_type_vars, FunSignature, FuncArg, Type, TypeEnum, TypeEnum::*, VarMap, + }, }; /// The parallelism mode within a block. @@ -392,8 +394,6 @@ fn gen_rpc_tag( buffer.push(b'k'); } - use nac3core::typecheck::typedef::TypeEnum::*; - let int32 = ctx.primitives.int32; let int64 = ctx.primitives.int64; let float = ctx.primitives.float; @@ -451,7 +451,7 @@ fn gen_rpc_tag( buffer.push(b'a'); buffer.push((ndarray_ndims & 0xFF) as u8); - gen_rpc_tag(ctx, ndarray_dtype, false, buffer)?; // Pass false for is_kwarg + gen_rpc_tag(ctx, ndarray_dtype, false, buffer)?; } _ => return Err(format!("Unsupported type: {:?}", ctx.unifier.stringify(ty))), } @@ -804,7 +804,7 @@ fn format_rpc_ret<'ctx>( Some(result) } -pub fn rpc_codegen_callback_fn<'ctx>( +fn rpc_codegen_callback_fn<'ctx>( ctx: &mut CodeGenContext<'ctx, '_>, obj: Option<(Type, ValueEnum<'ctx>)>, fun: (&FunSignature, DefinitionId), @@ -816,10 +816,6 @@ pub fn rpc_codegen_callback_fn<'ctx>( let int32 = ctx.ctx.i32_type(); let size_type = ctx.get_size_type(); let ptr_type = int8.ptr_type(AddressSpace::default()); - let tag_ptr_type = ctx - .ctx - .struct_type(&[ptr_type.into(), size_type.into()], false) - .ptr_type(AddressSpace::default()); let service_id = int32.const_int(fun.1 .0 as u64, false); // -- setup rpc tags let mut tag = Vec::new(); @@ -842,9 +838,8 @@ pub fn rpc_codegen_callback_fn<'ctx>( } else { let tag_len = tag.len(); let arr_ty = int8.array_type(tag_len as u32); - let tag_const = int8.const_array( - &tag.iter().map(|&b| int8.const_int(u64::from(b), false)).collect::>(), - ); + let tag_const = int8 + .const_array(&tag.iter().map(|&b| int8.const_int(u64::from(b), false)).collect_vec()); let arr_gv = ctx.module.add_global(arr_ty, None, &format!("{hash}.arr")); arr_gv.set_linkage(Linkage::Private); arr_gv.set_initializer(&tag_const); @@ -941,7 +936,7 @@ pub fn rpc_codegen_callback_fn<'ctx>( ctx, if is_async { "rpc_send_async" } else { "rpc_send" }, None, - &[service_id.into(), tag_ptr.into(), args_ptr.into()], + &[service_id.into(), tag_ptr.into(), arg_array.into()], Some("rpc.send"), None, ); -- 2.47.2 From bdfcd2dd1f445cf0141b89333a5e697519141197 Mon Sep 17 00:00:00 2001 From: ram Date: Mon, 10 Feb 2025 10:58:49 +0000 Subject: [PATCH 8/8] Merge upstream/master into feature/rpc-keywords --- nac3artiq/demo/module.elf | Bin 2940 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 nac3artiq/demo/module.elf diff --git a/nac3artiq/demo/module.elf b/nac3artiq/demo/module.elf deleted file mode 100644 index 1516a2479a74f16952fe19edb313ea76905e57df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2940 zcmb_ePiP!f7=N=nv)R}*u9=2LBeG=DvV?V#S`Sith_V&YATcPFB1|T;+w9WpChknM z5y6BesJE4+^x(k=5j=WmZXQ->pa+i%9y}DRc=2X6$r^V3{buG(CoN(Q{ouFn`}3Rc zee>Qo@9oDIre0ALMMz1CKLxEHf#-qIULjOp&k9ZC#N(phkHZxSxdeG4BckV}ztAHD zp~d1uX(1#Q;3raHJc02%;u5dICK%@&)4zGe?~qL5Y4`}n2whRS#_1=-4d}nGwh}kM zZEozidTLGR&;!2^*RtP}tIV(Z&+Ouvv@yDtHby>A>#EY*R2A?u!pLoPbWQCV`K`Zo zEh~)Ev~P6`llFu#Mrp5ijO^B4Fqc--wMMU=THlp(>1!%-6(ZY7XbFAr6nuXPeOc?C z)xMh46)mdm*8#1QJXP3jYO}%_+I}Xe1@)w~uCAm+Q=1mX$jx?BE3}Q=cBiRr3L{Hf zZ5v~>Cl$lIx!2Tg?Hl>+PEglIk#)suLf)=l+lCs=eWcpHR#C%5!pUqu^>A%ivpnZs z-w$e=Oc`Ue3;R)RIofKhww+6lwzftO*6qZ@wQ>Lab=iy7ZPc%IFQ~0GfOTc9d-vB( zd-KuOwyI;@GPq76*a{ZJ#oGY6!#`vxc^joxEJ@_u+`&PmCc!2M+E&ILOO&-UQ;Gb(8 zDbja08QA{sc=g!@d(IuN+0I%2$F6&o_r{B*>f(5P$+50{VAtne%PYB_xwIlIt7=zj zmSxT?Ij=MH76bL=nmHkc!nDeV(^E4-`AF&cVh88j@Ni$kk7W^`{-@jJS*u#zZnw~% zDXb}i{_{W?G1jJD^kWTWYEAhx@maFamln!!^2JTKd0q~I*MdHU@fVEu;L~}KLuN7R zBte#_hws{m>GxtBlooOFF}*=O%=1mme>ES2i+V$EKtc>WG87Av)i zXITi(xvu4U*i&I4sjyra&J}BZ*{&DE8UJ&@GDU)`j@Mlz)Sry3AU5)VJ^q0GGHl+H zlmXnFZ(v-8ev&@+{AuTPO*$zF4$9+jPD98S{)VOJ(bF-L4`aHLF~#gH~Fw&EiT;@CW2z zT&W7PY`bM)y37J6a}_Rve%TB=Cf+1`*sfL_VWL6sP#Kv}M^xqiD(nq{>mdL8426Bp zho#E(Adl<%2b@F>x)jM{FOl~ga02T`Oi5B0W1P>*e6!#X@5XpCAM;3F{J)c=O5P+K z)L9?*jo`j<|0s-c|6T*wN91w;3GTZT&PAC4*bC&bKZs4}avkJ%#9qf#KCgu!@eb_x zyqtR$U~YMC@a)LKmLmD{aQHk-%=3hh??|rHKY`9YW4`!*PE79$dCb`eNAmbh%I^}U TFEZbCSma0{?@MW5l@--QPk -- 2.47.2