cr1901 has quit [Read error: Connection reset by peer]
cr1901 has joined #amaranth-lang
<lsneff>
Is there a generic way to go from a `Value` to a `ValueCastable`?
<whitequark>
a `Value` is a `ValueCastable`
<whitequark>
this is not reflected by `isinstance()` and `issubclass()`; that's an unintentional omission
<whitequark>
strictly speaking, there are currently separate concepts of "value-castable" and `ValueCastable`; a "value-castable object" is anything `Value.cast` will accept, and a `ValueCastable` is anything implementing that interface
<whitequark>
they shouldn't be separate
<lsneff>
Sorry, XY problem. I want to Mux two things that aren't `Value`, but are `ValueCastable` and would like them to still be the original value castable on the way out
<whitequark>
from your description this is ill-defined
<whitequark>
supposing you have `a1, a2 = A(), A()` where `class A(ValueCastable):`. which object, or even the transformation of which object, should `Mux(sel, a1, a2)` return?
<lsneff>
My thought was that `ValueCastable` or something like it would have a method to cast a `Value` back into that structure. So, `Mux` would require that `a1` and `a2` are the exact same type, cast them into values, `Mux` them, and then cast the resulting value into `a1.__class__`
<whitequark>
this isn't currently possible, but I agree that this is technically feasible and a possible direction for language evolution
<whitequark>
there are several issues with taking the language in this direction
<whitequark>
so, all of the basic operations on values will coerce the non-`Value` operand to a `Value` using `Value.cast`
<lsneff>
I admit that it doesn't feel elegant to cast everything everywhere.
<whitequark>
what you're proposing (whether it's just for `Mux` or for all operations; I foresee people asking to expand this to every basic operation) is going to add a second level of coercion, which makes the mental model of how coercions work much more complicated
<lsneff>
Perhaps the concepts of `Value` and `ValueCastable` could be merged into a single kind of object to avoid extraneous casting?
<whitequark>
(it's not exactly coercion since it is the _return_ value that changes its type, but "coercion" seems like the closest related thing)
<lsneff>
Right
<whitequark>
you can't merge `ValueCastable` anywhere since the reason for its existence is not polluting the user object namespace with Amaranth names
<whitequark>
we had `UserValue` that worked similarly (you could conceivably enable the downstream code inherit from `Value` instead, the design is similar) and it was a mistake
<whitequark>
* downstream code to inherit from
<whitequark>
but I also don't think that this is the primary issue her
<whitequark>
s/her/here/
<modwizcode>
could you apply a model of coercion similar to how Rust does it?
<whitequark>
consider that a `ValueCastable` can return a value-castable object (it is not required to return a `Value`)
<modwizcode>
actually wait did i make up a rust semantic and then misremember my own made up semantic
<modwizcode>
i didn't mean coersions and i did make up a semantic that doesn't exist (i think), but i meant the way that from and into work
<modwizcode>
specifically into
<XMPPwocky>
whitequark: i mean, that's called double dispatch
<whitequark>
yes, but actually no
<modwizcode>
whitequark: oh this breaks my proposed model anyway
<whitequark>
if it was unrestricted double dispatch I would have vetoed this feature on language design grounds
<whitequark>
(I did this earlier when lkcl proposed it)
<modwizcode>
yeah i agree
<XMPPwocky>
the core principle is "subexpression looking at its superexpressions", i.e. making things an arbitrary DAG instead of just a tree
<XMPPwocky>
<modwizcode> "could you apply a model of..." <- actually i think the rust influence here is in `impl trait`
<modwizcode>
XMPPwocky: i was thinking of a specific thing, but perhaps
<whitequark>
so the reason I've called it a "coercion" is the similarity with e.g. the Python numeric classes: `int + int` gives you an int, `float + float` gives you a float
<whitequark>
the language implementation picks one of the semantically equivalent concrete functions by looking at argument types
<whitequark>
and ensures the return type matches it
<whitequark>
it's not actually a coercion because it requires that the types of arguments be exactly same, but it's vaguely similar
<XMPPwocky>
modwizcode: you say This Function Returns An X, Which Implements Foo. what is X? you don't get to know, ha. you can call X *only* by using it as an abstract Foo
<XMPPwocky>
but it's still a value type, it's not boxed/pointer'd
<modwizcode>
XMPPwocky: yeah that's true
<modwizcode>
wait, can't you just solve this by special casing an operation with two ValueCastables of the same class somehow? or with an additional type tag? and then that should "coerce" to value just fine?
<whitequark>
you can obviously special-case it, which is what Lachlan proposed
<whitequark>
I'm thinking that we probably shouldn't
<modwizcode>
right, i just misunderstood the nature of the request, my bad
<XMPPwocky>
rust also had to deal w/ this in the context of iterator adaptors
<XMPPwocky>
where you'd have incredibly cursed types like FlatMap<Zip<Foo, SomeFunctionThingy>> and that made 1. all the function signatures look terrible and 2. made your external API dependent on your internal logic, because your internal logic w/ iterator adaptors *is* a type
<XMPPwocky>
so people just made wrappers - `struct Foo(FlatMap<Zip....>>);`
<modwizcode>
XMPPwocky: I forgot what was done before `impl Trait`
<lsneff>
I think there’s a concrete difference between operators that just route data, like ‘Mux’, and operators that modify the values. I don’t think special casing things like Mux, etc to deal with user-defined types is intrinsically bad
<modwizcode>
I don't really see how this helps you though other than not wrapping the result type in a cast manually. I'm not sure I would say it's fair to force my personal preference onto others but that is part of language design. So my preference would be to enforce explicitly casting results for this use case.
<whitequark>
Lachlan: the extension you're proposing is going to be tempting to further extend to handle things like fixed point types
<XMPPwocky>
XMPPwocky: ultimately this worked but was unergonomic enough to make people mad, and then closure types broke it entirely (you can't name a closure type at all). this concludes Rust Storytime With Mimir
<modwizcode>
I don't like the idea of seeing Mux as fundamentally different at all. It should semantically match the functionality achieved through equivalent if statements.
<whitequark>
(really there should be an expression-level `Switch` that `Mux` would desugar into, but yes)
<cr1901>
I thought impl Trait doesn't prevent you from introspecting the actual type returned
<modwizcode>
whitequark: RFC?
<cr1901>
it's just a shorthand in return position for returning unnameable types
<modwizcode>
cr1901: You cannot introspect it in the method body itself is the point.
<whitequark>
modwizcode: doesn't need one until it becomes a part of the surface language
<modwizcode>
whitequark: RFC for the language addition was what I meant
<whitequark>
let's keep things on topic
<modwizcode>
modwizcode: Technically speaking it can be introspected. It just doesn't bring you useful value in general. What is important is that downstream consumers are unaware of the type at the type system level itself.
<modwizcode>
whitequark: Ah, yes. Sorry
<whitequark>
Amaranth doesn't have parametric polymorphism so I don't see how `impl Trait` is relevant
<lsneff>
I kind of just see Mux as an if expression (as opposed to statement), so it surprises me when the language requires the parameters/outputs to not be user-defined values.
<lsneff>
But I guess that may represent a move towards a higher-level language than amaranth is intended to be
<whitequark>
I think this has some parallels in C-family languages, where if/else is more powerful than ?:
<lsneff>
Good point — and rust, where if/else is an expression
<whitequark>
actually, that would only be true for C++ and even then I'd need to look at the spec
<whitequark>
it's still not the primary issue here, as I see it
<whitequark>
so; `Mux()` is similar to C's `?:`, and I think comparing the two is useful
<whitequark>
in C and C++, you can use `?:` on both RHS and LHS. in Amaranth, you can only use `Mux` on RHS
<whitequark>
this happens because C and C++ have a well-defined notion of an lvalue, and `?:` will coerce its last two operands to the same type, which can be an lvalue
<whitequark>
Amaranth does have an equivalent notion (it is called "assignment target" in the language reference), but `Mux` isn't one, and `ValueCastable` isn't one either
<whitequark>
value-castable objects can be assigned to by explicitly defining `.eq()` on them (which I think only has a single reasonable implementation, `return Value.cast(self).eq(other)`, otherwise `Cat(x).eq(...)` will have different behavior from `x.eq(...)`)
<whitequark>
not all `ValueCastable` implementers will lower to an assignment target expression (e.g. you could conceivably have a `ValueCastable` that always lowers to a constant), so it doesn't always include this `eq` implementation
mwk has quit [Ping timeout: 240 seconds]
<whitequark>
so, if you want your `ValueCastable` to be an assignment target, it needs to always lower to a combination of signals, slices/part selects, concats, and arrays
<whitequark>
how is this related to the `Mux` issue? well, like Lachlan correctly observes, `Mux` is an equivalent of If/Else or Switch in the expression language
<whitequark>
it currently lets you write code that is equivalent to adding `tmp = Signal(); with m.If(sel): m.d.comb += tmp.eq(if1) with m.Else(): m.d.comb += tmp.eq(if0)`, but without the intermediate signal and the verbosity
<whitequark>
what _should_ happen instead is treating the expression returned by `Mux()` as-if it was replaced by `if0` when `~sel` and `if1` when `sel`
<whitequark>
which would let you use it on the left-hand side of an assignment
<whitequark>
you might observe that we already have a primitive that does exactly that; it's called `Array`. and yes. you could redefine `Mux` to return `Array([if0, if1])[sel]`, and that would work as it should, at the cost of worse codegen
<whitequark>
what this does, on the RTL level, is implicitly converts every assignment to an `Array` (with a variable index) into a nested `Switch`, and then handling every case individually. in the backend this is called "legalization"
<whitequark>
(you might ask, what if I do `x, y = Signal(), Signal(); m.d.comb += Cat(array1[x], array2[y]).eq(...)`? well, it expands to a combinatorial product)
<whitequark>
this is what my earlier "expression-level Switch" remark was about. instead of making this expansion only accessible through `Array` (and also certain part-select expressions), we should expose the "please expand this expression into a decision tree during codegen" primitive directly, reimplement `Array` in terms of it, reimplement `Mux` to use it, and then expose it to downstream code
<whitequark>
this was a very long digression on my end; ultimately my point is that `Mux` is not currently like an "if expression", but it _should be_, and there is a specific design that would make it one
<whitequark>
really it's just a pure function. `(Repl(sel, max(len(if0), len(if1))) & if1) | (~Repl(sel, max(len(if0), len(if1))) & if0)` is an implementation just as valid as the current one, though obviously generating inefficient RTL
<whitequark>
now, let's get back on track discussing Lachlan's proposal
<whitequark>
any extensions to the behavior of `Mux` should take into account the fact that it ought to be an "if expression", but currently isn't
<_whitenotifier-e>
[YoWASP/yosys] ... and 34 more commits.
<whitequark>
the proposal seems fine in that regard; the suggested casting would work fine, since the branches of a "switch expression" would all need to be value-castable objects that, if the switch expression is used on the left-hand side, are assignment targets
<whitequark>
adding special behavior to `Mux` or the hypothetical future "switch expression" that `Mux` would desugar to isn't necessarily inherently a problem; yes, it is weird, but it is true that a "switch expression" is a special construct that generally needs special treatment, and the limited nature of the introduced polymorphism bounds the cognitive overhead required to track it (whether it's worth the benefits is still an open question)
<whitequark>
what bothers me is one specific issue: Lachlan's proposed behavior is unique (nowhere else does Amaranth use multiple dispatch where a *combination* of argument types triggers a specific behavior; in case of a "switch expression" this becomes "the type of the value of every branch must be the same"), and while it's trying to address a semantic disparity incidentally introduced by our syntax, it *looks* like something entirely different
<whitequark>
specifically, it looks like a general-purpose AST wrapping mechanism that is for some reason restricted to `Mux` alone
<whitequark>
if you extended it to handle asymmetric operand types (using Python's coercion rules) and then applied to the rest of the base language, it'd let you have a `ValueCastable` derivative that can be used in arithmetic/logical operations with `Value`s in such a way that you always get that derivative back; something that people requested in the past, and something that I think shouldn't be a part of the core language
<whitequark>
to summarize: this proposal adds a mechanism to the language that adds several features (multiple dispatch; the concept of casting back to a concrete type of a `ValueCastable`; the concept of limited polymorphism) all of which are unprecedented elsewhere in the language, and some of which have drawbacks (I'm concerned about requiring the constructor of the `ValueCastable` to have a `__init__(self, arg: Value)` form: this caused problems
<whitequark>
with `Record.like` earlier, and goes against our principle of not restricting user code in its behavior if at all technically possible); but on top of that it also partially replicates another feature that I explicitly rejected
<whitequark>
actually, I'm going to expand on that drawback, because I think it just makes the proposal unviable
<whitequark>
Lachlan: your proposal has an unstated assumption that the class of a `ValueCastable` always carries enough information to construct an instance wrapping a `Value`
<whitequark>
but this is almost never true
<whitequark>
data.View from the aggregate data types RFC requires the layout; a hypothetical fixed-point number proposal would require the position of the point; and so on
<whitequark>
for the same reason, you can't tell if two `ValueCastable`s are compatible (i.e. whether it makes sense to wrap the result of `Mux` into an instance of that `ValueCastable` at all) by comparing classes alone
indy has joined #amaranth-lang
cr1901 has quit [Read error: Connection reset by peer]
<cr1901>
Is the idea "if "x" is a ValueCastable that impls .eq(), you get Cat(x).eq for free?"
<whitequark>
no
<whitequark>
implementing `.eq()` on a `ValueCastable` has no bearing on anything other than calling `.eq()` on instances of that specific ValueCastable
<cr1901>
Then why does Cat() break if you don't impl .eq() in that specific way?
<cr1901>
Are there other valid impls of .eq() for a ValueCastable?
<whitequark>
nothing technically breaks; it's just that if `Cat(x).eq(...)` does not do the same thing as `x.eq(...)` it will probably make the users of your `ValueCastable` very sad at some point
<whitequark>
there are probably reasonable cases for `x.eq(...)` performing a superset of what `Value.eq(...)` does, but it's hard to justify
<cr1901>
Should there be a mixin/decorator/whatever to add eq() to ValueCastable w/ the impl you mentioned above then?
<cr1901>
(to avoid boilerplate)
<whitequark>
one line is hardly "boilerplate"
<cr1901>
Maybe, but now I can't unsee those two* lines ("def eq(self, other):") as a "optional functionality" for your classes; either ".eq() works" or ".eq() doesn't work"
<cr1901>
Hmmm, that didn't come out as clear as I'd hoped
<cr1901>
I'm hung up on "What's the point of writing .eq() out for each ValueCastable if there's probably only one correct way to do it?"
<whitequark>
you are still going to write an implementation. the question is whether this implementation will involve indirection or not
<cr1901>
If it's indirect (provided by amaranth), if "the one correct way" to write ".eq()" changes in the future, then ValueCastables keep working w/o user updating their code.
<Sarayan>
Just curious, what is ValueCastable and why is it important?
<whitequark>
cr1901: changing "the one correct way" would be a breaking change either way
nelgau has quit [Ping timeout: 248 seconds]
nelgau has joined #amaranth-lang
<cr1901>
whitequark: To be clear (and for future-me), I meant "If it's indirect (provided by amaranth package*)". But I see your point now.
<cr1901>
If the ".eq" mixin/whatever is provided by amaranth, and "the one correct eq method" impl changes, it probably broke more than ValueCastables that have an .eq() method