Wyatt Kennedy
02/15/2022, 5:51 AMclass TestInnerClass {
var x : Float = 0f
var y : Float = 0f
}
class TestClass {
val bar : TestInnerClass! // the exclamation point makes it a value property declaration. Initialization rules are the same as type TestInnerClass
var bar2 : TestInnerClass! // ERROR : value types cannot be mutable, since they aren't references
}
fun semanticExamples() {
val foo = TestClass()
foo.bar.x = 6f
val test : TestInnerClass! = foo.bar // FINE: When used on a function local variable, TestInnerClass! is essentially a const ref in c++
// when compiled, all usages of test can be replaced by foo.bar.
val test2 : TestInnerClass = foo.bar // ERROR references of this type can be assigned elsewhere, would allow undefined behavior when foo is destroyed.
val test3 : TestInnerClass? = foo.bar // ERROR same as above as well as not being able to be a nullable reference
val foo2 = TestClass()
foo.bar = foo2.bar // ERROR: value properties cannot be assigned because they are not references and implicit copy constructors do not exist
takesTestInnerClassRef(foo.bar)
functionTypeExample {
foo.bar.x = 6f // the value type can still be referenced because the reference to foo is enclosed.
test.x = 6f // ERROR: cannot be sure that the owner of test hasn't been garbage collected
}
}
// Types TestInnerClass and TestInnerClass? are demotable to TestInnerClass! which is least permissive
// The value type "!" annotates that the reference cannot be assigned to anything.
fun takesTestInnerClassRef(testInnerClass: TestInnerClass!) {
val test : TestInnerClass! = testInnerClass // still valid because they are basically just const refs
val foo2 = TestClass()
foo2.bar = testInnerClass // Error: value properties cannot be assigned to as mentioned above
}
fun functionTypeExample(func: () -> Unit) {
}
Most of these are simple semantic rules in the type system, the only compile issue being able to use indirection in JVM without a reference being managed by the garbage collector, (not sure if it doesn't already support this). This syntax being purely ad-hoc discourages it's use unless someone explicitly needs better control of memory locality for high performance sections.
If you also introduced a value generic, you could also use this for compile time fixed length arrays of value objects should people want them.
// a way to declare value generics, I'm sure this has been discussed elsewhere
// |
// v
class FixedArray<T : Any!, val Length : Int> {
// ...
}
class Test {
var x : Float = 0f
var y : Float = 0f
}
fun valueArrayTest() {
val foo = FixedArray<Test!, 6>()
}
While the semantic rules could apply to all compilation targets, (JS, JVM, Native, etc), you could just provide a warning that value annotations will be ignored in environments where it's simply not possible to enforce, (probably JS). Is any/all of this something that has already been made permanently off the table?Ilmir Usmanov [JB]
02/15/2022, 7:20 AMmcpiroman
02/15/2022, 11:44 AMIlmir Usmanov [JB]
02/15/2022, 6:56 PMWyatt Kennedy
02/15/2022, 6:58 PMWyatt Kennedy
02/15/2022, 7:44 PMvalue class Test {
val bar1 = 0.5f
val bar2 = 0.5f
}
value class TestOuter {
cov /* copy val*/ test = Test()
}
fun test() {
var foo = TestOuter() // here foo is heap allocated, and lets say it points to 0x1234
foo.test.bar1 = 0.6f // were test not a value class, it would simply mutate the object
// however a new instance of Test is implicitly heap allocated, and test now points to 0x4444 (somewhere in heap)
// to hold the new result.
}
// results in heap allocations like so
TestOuter@0x1234 [
Test@0x3232 // originally pointed to TestOuter@0x3030
]
Test@0x3030 [
bar1 = 0.5f
bar2 = 0.5f
]
Test@0x3232 [
bar1 = 0.6f
bar2 = 0.5f
]
My reservations with this syntax aside, (implicit heap allocation is a dangerous thing), it actually has valid use cases that are not related with my suggestion. My suggestion would be to enforce certain objects not being allocated to heap altogether, but with many semantic restrictions to enforce it. For example:
data class Test (
val bar1 = 0.5f
val bar2 = 0.5f
)
class TestOuter {
var test = Test!()
}
fun test() {
var foo = TestOuter() // here foo is heap allocated, and lets say it points to 0x1234
foo.blah = Test(2.0f, 3.0f) // allow only constructors
}
// there is then only one heap allocation.
TestOuter@0x1234 [
// Test implied
test = {
bar1 = 2.0f
bar2 = 3.0f
}
]
This prevents the heap allocation of an instance of test altogether, removes an indirection when accessing properties, and makes it impossible to produce a cache miss because of that lack of indirection. If you can have fixed length arrays, this becomes very prudent. For example:
fun test() {
// with normal references
val foo = Array<Test>(6)
for (i in 0..foo.size()) {
foo[i] = Test()
}
// with value types
val foo2 = FixedArray<Test!, 6>(::Test) // You will probably require that a default constructor be required, or some other way to provide an initializer at compile time not exactly sure what the syntax might be around this
}
// first type
Array@1234 {
Test@3232
Test@3434
Test@3535
Test@3636
Test@3737
Test@3838
}
// ... and then six more allocations that look like this at each of those references scattered across heap. This can result in cache misses on iteration.
Test@3232 {
bar1 = 0.5f
bar2 = 0.5f
}
Wheras the second example is a single fixed length allocation.
Array@1234 {
{
bar1 = 0.5f
bar2 = 0.5f
}
{
bar1 = 0.5f
bar2 = 0.5f
}
// etc
}
The tradeoff is that the references cannot simply be passed around and stored in other objects, as there lifetime is not managed individually by the GC and this can be enforced semantically, but then you benefit from many fewer cache misses.Wyatt Kennedy
02/17/2022, 1:11 AM