evincarofautumn / kitten

A statically typed concatenative systems programming language.
http://kittenlang.org/
Other
1.09k stars 39 forks source link

Copy constructors and destructors #156

Open evincarofautumn opened 8 years ago

evincarofautumn commented 8 years ago

In the new compiler, there is a “linearisation” pass that introduces calls to copy constructors (when a variable is used multiple times) and destructors (when a variable is not used):

// Sweetened:
-> x, y;
x x x

// Unsweetened:
-> y;
-> x;
local.0        // Move x to stack.
local.0        // Move x…
_::clone::<…>  // …and call its copy constructor.
local.0        // Every subsequent use of x…
_::clone::<…>  // …generates a clone call.
local.1        // Move y…
_::destroy<…>  // …and call its destructor, because it was not used.
_::drop<…>

This is intended to support smart resource-management types in the common vocabulary, such as:

These traits might have types like:

trait clone<T> (T -> T)
trait destroy<T> (T -> T)

But this raises a few issues:

This is a lot of machinery, and I’m not sure how to deal with the last point. There are a few possibilities:

  1. Lift permissions to the caller, as usual. This could add noise to locally stateful/resourceful functions that are otherwise pure.
  2. Hide permissions from the caller. This could lead to subtle bugs.
  3. Force implementations to use permissions unsafely, e.g., using with (+IO). This is basically the same as (2), but explicit.

None of these is ideal.

In addition, destructors interact poorly with tail-call optimisation. A call in tail position might not be TCO’d if there’s an implicit destructor call after it, which breaks the expectation of guaranteed TCO.

So the question is: do I keep this and try to work through whatever issues arise in practice, or do I scrap it in favour of some more functional approach to resource management?

evincarofautumn commented 7 years ago

Here’s an alternative design, which seems better:

If a variable is mentioned multiple times, it needs the clone trait. If using the default clone implementation, the value is copied implicitly with memcpy. If using a custom clone implementation, or if a field has a custom clone, then each appearance of the variable (except for one?) must be followed immediately by a call to clone (or perhaps a special copy operator, so we have more information). Primitives would all be implicitly copyable.

As in the mini-kitten reference typechecker, it would also be possible to allow explicit moving with e.g. a move operator, which would cause a variable to go out of scope.

If a variable is mentioned zero times, it needs the drop trait. Even though I like the idea of explicit copy constructor calls, I’m loath to require explicit destructor calls.

This will be easier to implement if we do a transformation that we need to do anyway: lifting the relative (De Bruijn) indices of locals into absolute indices. With this, we can track non-nested variable lifetimes, and also allocate/deallocate space for all locals on function entry/exit.