Constant Default Parameter Support #98

Merged
sb10q merged 7 commits from default_parameter into master 2021-11-23 07:32:09 +08:00
2 changed files with 95 additions and 7 deletions
Showing only changes of commit c7b6b048fb - Show all commits

View File

@ -1065,10 +1065,23 @@ impl TopLevelComposer {
and names thould not be the same as the keywords"
.into());
}
args.args
let mut arg_with_defualt: Vec<(&ast::Located<ast::ArgData<()>>, Option<&ast::Expr>)> = args
.args
.iter()
.map(|x| -> Result<FuncArg, String> {
.rev()
.zip(args
.defaults
.iter()
.rev()
.map(|x| -> Option<&ast::Expr> { Some(x) })
.chain(std::iter::repeat(None))
).collect_vec();
arg_with_defualt.reverse();
arg_with_defualt
.iter()
.map(|(x, default)| -> Result<FuncArg, String> {
let annotation = x
.node
.annotation
@ -1120,7 +1133,12 @@ impl TopLevelComposer {
Ok(FuncArg {
name: x.node.arg,
ty,
default_value: Default::default(),
default_value: default.map(|default| {
match Self::parse_parameter_default_value(default) {
Ok(res) => res,
Err(err) => panic!("{}", err),
}
}),
})
})
.collect::<Result<Vec<_>, _>>()?
@ -1272,7 +1290,20 @@ impl TopLevelComposer {
}
let mut result = Vec::new();
for x in &args.args {
let mut arg_with_defualt: Vec<(&ast::Located<ast::ArgData<()>>, Option<&ast::Expr>)> = args
.args
.iter()
.rev()
.zip(args
.defaults
.iter()
.rev()
.map(|x| -> Option<&ast::Expr> { Some(x) })
.chain(std::iter::repeat(None))
).collect_vec();
arg_with_defualt.reverse();
for (x, default) in arg_with_defualt {
let name = x.node.arg;
if name != zelf {
let type_ann = {
@ -1317,8 +1348,15 @@ impl TopLevelComposer {
let dummy_func_arg = FuncArg {
name,
ty: unifier.get_fresh_var().0,
// TODO: default value?
default_value: None,
default_value: default.map(|default| {
if name == "self".into() {
panic!("`self` parameter cannot take default value at {}", x.location)
Outdated
Review

Technically CPython lets you name self whatever you want, it's only a convention.
So this test should be "the function is a method and we are looking at its first parameter".

Technically CPython lets you name ``self`` whatever you want, it's only a convention. So this test should be "the function is a method and we are looking at its first parameter".

Would it be acceptable that in nac3, we just enforce and document that the self should always be self and should not be named arbitrarily? As I think this would not cause very big problems and our existing nac3 codebase is already doing this (e.g., here, here, and here)

Would it be acceptable that in nac3, we just enforce and document that the `self` should always be `self` and should not be named arbitrarily? As I think this would not cause very big problems and our existing nac3 codebase is already doing this (e.g., [here](https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3core/src/codegen/expr.rs#L293), [here](https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3core/src/codegen/expr.rs#L348), and [here](https://github.com/m-labs/artiq/blob/nac3/artiq/language/core.py#L56))
Outdated
Review

It is acceptable but not super clean - is it difficult to use the condition I proposed instead?

https://github.com/m-labs/artiq/blob/nac3/artiq/language/core.py#L56 is unavoidable without a lot of code (AFAICT) because CPython does not tell you if something is a method until it is bound, but inside NAC3 we have more information.

It is acceptable but not super clean - is it difficult to use the condition I proposed instead? https://github.com/m-labs/artiq/blob/nac3/artiq/language/core.py#L56 is unavoidable without a lot of code (AFAICT) because CPython does not tell you if something is a method until it is bound, but inside NAC3 we have more information.
}
match Self::parse_parameter_default_value(default) {
Ok(res) => res,
Err(err) => panic!("{}", err),
}
})
};
// push the dummy type and the type annotation
// into the list for later unification

View File

@ -1,3 +1,8 @@
use std::convert::TryInto;
use nac3parser::ast::{Constant, Location};
use crate::symbol_resolver::SymbolValue;
use super::*;
impl TopLevelDef {
@ -341,4 +346,49 @@ impl TopLevelComposer {
}
Ok(result)
}
pub fn parse_parameter_default_value(default: &ast::Expr) -> Result<SymbolValue, String> {
fn handle_constant(val: &Constant, loc: &Location) -> Result<SymbolValue, String> {
match val {
Constant::Int(v) => {
if let Ok(v) = v.try_into() {
Ok(SymbolValue::I32(v))
} else {
Ok(SymbolValue::I64(v.try_into().unwrap()))
ychenfo marked this conversation as resolved Outdated

Should we just accept int32? int64 is handled in the call part, e.g. int64(1234).

Should we just accept int32? int64 is handled in the call part, e.g. `int64(1234)`.

this is handled in the new commit below

this is handled in the new commit below
}
ychenfo marked this conversation as resolved Outdated
Outdated
Review

Isn't that an internal compiler error if this code is executed, not a problem with the user code?
Using unreachable!() sounds more appropriate here.

Isn't that an internal compiler error if this code is executed, not a problem with the user code? Using ``unreachable!()`` sounds more appropriate here.
Outdated
Review

Isn't that an internal compiler error if this code is executed, not a problem with the user code?
Using unreachable!() sounds more appropriate here.

Isn't that an internal compiler error if this code is executed, not a problem with the user code? Using ``unreachable!()`` sounds more appropriate here.

No I don't think so? It is possible for users to specify some default value outside the range of int64. I think a more appropriate error message would be value out of range and only say int64 if the value is really an int64?

No I don't think so? It is possible for users to specify some default value outside the range of int64. I think a more appropriate error message would be value out of range and only say `int64` if the value is really an int64?
Outdated
Review

OK, yes.

OK, yes.

Thanks, this error message is updated in the new commit

Thanks, this error message is updated in the new commit
}
Constant::Float(v) => Ok(SymbolValue::Double(*v)),
Constant::Bool(v) => Ok(SymbolValue::Bool(*v)),
Constant::Tuple(tuple) => Ok(SymbolValue::Tuple(
tuple.iter().map(|x| handle_constant(x, loc)).collect::<Result<Vec<_>, _>>()?
)),
_ => unimplemented!("this constant is not supported now at {}", loc),
}
}
match &default.node {
ast::ExprKind::Constant { value, .. } => handle_constant(value, &default.location),
ast::ExprKind::Call { func, args, .. } if {
match &func.node {
ast::ExprKind::Name { id, .. } => *id == "int64".into(),
_ => false,
}
} => {
Outdated
Review

Aren't we matching on id(numpy.int64) elsewhere?
https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3artiq/src/lib.rs#L270-L306
I'm fine with string matches since they're simpler and potentially faster, but we should do things consistently.

Aren't we matching on ``id(numpy.int64)`` elsewhere? https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3artiq/src/lib.rs#L270-L306 I'm fine with string matches since they're simpler and potentially faster, but we should do things consistently.

I think here we are handling default parameters in nac3core so we do not really know the id(numpy.int64)?

In type inference module we also do things using strings matches here and here. So I think it is ok to use strings? Or did I miss anything?

I think here we are handling default parameters in nac3core so we do not really know the `id(numpy.int64)`? In type inference module we also do things using strings matches [here](https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3core/src/typecheck/type_inferencer/mod.rs#L659) and [here](https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3core/src/typecheck/type_inferencer/mod.rs#L624). So I think it is ok to use strings? Or did I miss anything?

Aren't we matching on id(numpy.int64) elsewhere?
https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3artiq/src/lib.rs#L270-L306
I'm fine with string matches since they're simpler and potentially faster, but we should do things consistently.

That is matching on the value type, not about functions.

> Aren't we matching on `id(numpy.int64)` elsewhere? https://git.m-labs.hk/M-Labs/nac3/src/branch/master/nac3artiq/src/lib.rs#L270-L306 I'm fine with string matches since they're simpler and potentially faster, but we should do things consistently. That is matching on the value type, not about functions.
Outdated
Review

What's the difference? There isn't one in CPython.
Also this can cause issues e.g. if the user types from numpy import int64 as i64. CPython and nac3artiq will expect i64, whereas nac3core will expect int64.
If that's the simplest thing to do, I'm fine having restrictions such as "numpy types must be imported as their reserved NAC3 keywords int64/int32/..." but they should be documented and enforced by the compiler.

What's the difference? There isn't one in CPython. Also this can cause issues e.g. if the user types ``from numpy import int64 as i64``. CPython and nac3artiq will expect ``i64``, whereas nac3core will expect ``int64``. If that's the simplest thing to do, I'm fine having restrictions such as "numpy types must be imported as their reserved NAC3 keywords int64/int32/..." but they should be documented and enforced by the compiler.
Outdated
Review

Anyway this isn't the most pressing issue at the moment and we can do this inconsistent string/id match for now. Let's continue this discussion in #105

Anyway this isn't the most pressing issue at the moment and we can do this inconsistent string/id match for now. Let's continue this discussion in https://git.m-labs.hk/M-Labs/nac3/issues/105
if args.len() == 1 {
match &args[0].node {
ast::ExprKind::Constant { value: Constant::Int(v), .. } =>
Ok(SymbolValue::I64(v.try_into().unwrap())),
_ => panic!("only allow constant integer here at {}", default.location)
}
} else {
panic!("only allow constant integer here at {}", default.location)
}
}
ast::ExprKind::Tuple { elts, .. } => Ok(SymbolValue::Tuple(elts
.iter()
.map(|x| Self::parse_parameter_default_value(x))
.collect::<Result<Vec<_>, _>>()?
Outdated
Review

Lists should also be supported (also if they are a module global, see comment below).

Lists should also be supported (also if they are a module global, see comment below).

Lists are not immutable, I think we discussed this previously?

M-Labs/nac3-spec#6

Function parameter defaults for kernel, portable, and rpc functions are very important for us. The limitation of only allowing immutable or primitive default types is fine.

Lists are not immutable, I think we discussed this previously? https://git.m-labs.hk/M-Labs/nac3-spec/issues/6 > Function parameter defaults for kernel, portable, and rpc functions are very important for us. The limitation of only allowing immutable or primitive default types is fine.
Outdated
Review

Indeed, and that's a well-known pitfall in CPython as well.
ARTIQ drivers do use lists with default parameter. AFAICT this can be changed to tuple, so far...

Indeed, and that's a well-known pitfall in CPython as well. ARTIQ drivers do use lists with default parameter. AFAICT this can be changed to tuple, so far...
Outdated
Review

Well we can start with tuples only for now and see if that causes any serious problems.

Well we can start with tuples only for now and see if that causes any serious problems.
)),
_ => unimplemented!("only constant default is supported now at {}", default.location),
}
}
Outdated
Review

We also want to support globals defined in the module, e.g.:

FOO = 1234

class XXX:
   def yyy(self, z: int32 = FOO):
      ...

See the ARTIQ drivers, this is used in many places.

We also want to support globals defined in the module, e.g.: ```python FOO = 1234 class XXX: def yyy(self, z: int32 = FOO): ... ``` See the ARTIQ drivers, this is used in many places.
Outdated
Review

In fact, why not just compile the expression like any other?
Things like def foo(x=[i*2 for i in range(3)]) are also valid Python.

In fact, why not just compile the expression like any other? Things like ``def foo(x=[i*2 for i in range(3)])`` are also valid Python.

The expression should be immutable and primitive types/tuples. I think we can eval() the expression and set the default value if its type can be accepted.

The expression should be immutable and primitive types/tuples. I think we can `eval()` the expression and set the default value if its type can be accepted.
Outdated
Review

eval is slow and not available in nac3standalone.

``eval`` is slow and not available in nac3standalone.

Regarding performance issue, I think we can optimize it later if it is really that slow. For nac3standalone, I think we can support simple cases first and leave this later, I don't think this is of high priority?

Regarding performance issue, I think we can optimize it later if it is really that slow. For nac3standalone, I think we can support simple cases first and leave this later, I don't think this is of high priority?
Outdated
Review

Well the only thing we really need is module globals. If that can be done then something like the current code seems fine.

Well the only thing we really need is module globals. If that can be done then something like the current code seems fine.

Ok I will add module globals support on top of the current constant support

Ok I will add module globals support on top of the current constant support

Sorry, still one thing not very clear about this...

To handle complex expressions(calling eval for now? Also, expressions like Module.T which refers to things defined in another module imported) as default value, should we add a function in symbol resolver, which accepts a ast::Expr and return a SymbolValue?

Sorry, still one thing not very clear about this... To handle complex expressions(calling `eval` for now? Also, expressions like `Module.T` which refers to things defined in another module imported) as default value, should we add a function in symbol resolver, which accepts a `ast::Expr` and return a `SymbolValue`?
Outdated
Review

Let's not call eval and let's keep the more restricted approach. Keep the current mechanism and simply add support for being able to use globals e.g.:

FOO = 1234

class XXX:
   def yyy(self, z: int32 = FOO):
      ...
Let's not call ``eval`` and let's keep the more restricted approach. Keep the current mechanism and simply add support for being able to use globals e.g.: ```python FOO = 1234 class XXX: def yyy(self, z: int32 = FOO): ... ```

I have added the basic module primitive/tuple global default parameter in the new commit.

No eval is used, only simple variable name is supported, but still involve some changes in symbol resolver(adding a function in symbol resolver which accepts a ast::Expr and return a SymbolValue). If I do not miss anything, I do not think there is another way to workaround this without changing symbol resolver... since in nac3artiq the core compiler does not know anything about the module globals if symbol resolver does not provide any information to it. Please have a review, thanks!

I have added the basic module primitive/tuple global default parameter in the new commit. No `eval` is used, only simple variable name is supported, but still involve some changes in symbol resolver(adding a function in symbol resolver which accepts a `ast::Expr` and return a `SymbolValue`). If I do not miss anything, I do not think there is another way to workaround this without changing symbol resolver... since in nac3artiq the core compiler does not know anything about the module globals if symbol resolver does not provide any information to it. Please have a review, thanks!
Outdated
Review

LGTM

LGTM
}