-
-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for struct/record types #418
Conversation
Hi. I have not yet done a detailed review, but let start by saying that I am extremely excited about this PR! This is definitely something I was planning to add to the language.
👍
I feel a bit bad to go back to the design level here because you already added this complete implementation including documentation, error handling, etc. But before we go ahead with this, I would really like to understand if structural record types are the best choice for Numbat, or if we should rather go with a nominative system. I am familiar with structural record types from languages like PureScript. If you don't have an additional (strong) newtype mechanism on top, there's the obvious problem of record types colliding with other (structurally same) record types of completely different semantic meaning. For example, you might want to have I think my first impulse would have been to add more "usual" struct/record types to Numbat that would be based on the name of the type.. with a Rust-like syntax: struct WorldCoordinate {
x: Length,
y: Length,
}
struct LocalCoordinate {
x: Length,
y: Length,
}
fn world_to_local(pos: WorldCoordinate) -> LocalCoordinate = … I'm curious why you decided to go the other way? Would a nominative record type be harder to implement? |
This was pretty much the reason, with named structs the parser would either need to become context sensitive, or we add an extra step after parsing or during typechecking to resolve whether an identifier is a dimension or a struct name. I think the implementation of structs in the evaluator can stay the same, I'm happy to work on changing this to a nominal system. Do you have any thoughts on the best way to go about parsing and disambiguating struct and dimension names? |
It would be great if we could explore this. If it turns out to be more complex than we think now, please feel free to discuss it first before spending a lot of effort.
So before we talk about implementation, maybe we should consider the language design first. Is it confusing for users to have both struct types (and later maybe enums, type aliases, …) in the same namespace as the dimension types? I mean, we have the same problem with the other builtin types (String, Bool, DateTime, …), but it might be nice to know from reading the code whether something less familiar like Which leaves us with the problem you mentioned. In the parser, we don't know what we are parsing when we see We could generalize this to a "name resolution" stage that would solve both the prefix and the dimension-vs-normal-type ambiguity. It would rely on seeing the Another option which might be more reasonable is to do it in the type checker. We already add all dimension types to In both cases, we would have to modify the parser.. at least naming-wise. What is now called Do you think that could work, or do you see additional problems? |
Just a side note regarding your current implementation: the record types do not work with generics.
This is not a problem of your change, but more a problem with my buggy generics implementation… which does not properly distinguish between type parameters and concrete types in some cases (see #166 and a non-ready attempt to resolve it: #253). Just wanted to mention it. I would probably skip generics for structs in the first iteration. Later, something like
might be nice to have. But there are a lot of other things that are more important 😄. |
This makes sense and I think it's the best route to take, I'll have a play around with implementing this.
Yeah, I suppose I'll keep structs and dimensions separate in the typed AST, and do the renaming of
Ah, I actually made this work for return types, are parameter generics significantly different? |
2e11a7f
to
5d3ff85
Compare
I've made structs nominally typed using the syntax you've proposed, I also refactored a small amount how name collisions are handled so that checking for name collisions between items in the 'type' namespace (and also 'value' namespace) are a bit simpler (however I didn't refactor the collision checking that occurs within the prefix transformer) As for disambiguating dimensions and struct names, I simply check if a type expression is only a type identifier with a name matching a struct, and take it to be a struct type here: https://github.com/simmsb/numbat/blob/simmsb/struct-types/numbat/src/typechecker.rs#L1835-L1842. I think this is sound as a dimension name cannot clash with a struct. |
Thank you very much for the update — I tried a few things and it looks great! Some remarks before I dive deeper into the review:
|
I've fixed all the tests and CI, there's now tests for parsing around structs and some more tests for type checking of structs, and name collisions. Struct names do collide properly with dimension names, but not with unit, variable, and function names.
|
Type::Struct(StructInfo { | ||
definition_span, | ||
name, | ||
fields, | ||
}) => Type::Struct(StructInfo { | ||
definition_span: *definition_span, | ||
name: name.clone(), | ||
fields: fields | ||
.into_iter() | ||
.map(|(n, (s, t))| { | ||
( | ||
n.to_owned(), | ||
(s.clone(), apply_substitutions(t, substitute, substitutions)), | ||
) | ||
}) | ||
.collect(), | ||
}), | ||
type_ => type_.clone(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this code even trigger? We can't have structs with generic parameters, can we? That would require something like (copying Rust syntax, again):
struct Vec<Coordinate> {
x: Coordinate,
y: Coordinate,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I finished my review. Sorry for taking a bit longer. This looks really great! Most comments are just minor naming/clarification things. Let me know if you need any help with finishing this. I'm really looking forward to integrating this.
Another small thing that I found: This leads to an error:
but this doesn't:
Edit: I fixed this. |
57ec8b4
to
e824cfb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made some changes myself. Hope that's okay. There are two remaining open comments/questions, which I do not see as blocking issues that would prevent me from merging this. Maybe we can come back to them at some point.
Nice, thanks for taking the time to make those changes :) |
FYI: we're already making use of structs in a new feature: #431 |
Hey, I've been playing with numbat recently and I've found the type system to work perfectly for my use case (calculating inductor, resistor, etc parameters for a boost converter flashlight driver), however the lack of record types is causing me to repeat myself a bit too much.
Here I've added an implementation of structural record types that are used like so:
The choice of the
${
syntax for records was because it looked fairly easy to add unambiguously.Records are implemented in the interpreter with a new value type, and two new opcodes:
Value::Struct(Arc<[(String, usize)]>, Vec<Value>)
Contains the field metadata of the struct (the list of field names and mapped value locations), and the list of values of the struct. The field metadata is only used when printing the fields of the struct.
Op::BuildStruct
Takes two parameters: a 'struct metadata index' and the number of fields, uses the metadata index to retrieve the struct field metadata from the VM, then pops N values from the stack and places them and the metadata in a struct value
Op::DestructureStruct
Takes one parameter: the index into the struct to retrieve. Pops a struct from the stack and pushes back on the value at the given index.
Please feel free to decline this PR, or request significant changes if you intended to go with a different direction for record types :)