StanzaOrg / lbstanza-old

L.B. Stanza Programming Language
Other
216 stars 23 forks source link

Parametric Types in Lostanza #177

Closed callendorph closed 1 year ago

callendorph commented 1 year ago

Hi,

I'm trying to write a parametric type for a wrapper around the GSL Polynomial implementation. The GPoly has two types - real valued coefficients and complex coefficients. I'm trying to write a Parametric typed implementation. I think I'm stumbling on both a lack of understanding of the parametric typing and an error message that is particularly difficult to understand when debugging.

I first tried doing something like this:

public lostanza deftype GPolynomial<T> <: Unique & Lengthable :
  raw:ptr<double>
  size:int
  coeffs:ref<Tuple<T>>

public lostanza defn GPolynomial<?T> (C:ref<Tuple<?T>>) -> ref<GPolynomial<T>> :
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  ; Oversized but I can't get sizeof to work on T
  val sizeT = 2 * sizeof(double) ;  sizeof(T)

  val buf = call-c clib/malloc( N * sizeT )
  val ret = new GPolynomial<T>{buf, N, C}

  ; Initialize the Raw Representation.
  for (var i:int = 0, i < N, i = i + 1) :
    match(C.items[i]):
      (x:ref<Double>) :
        ret.raw[i] = x.value as double
      (z:ref<GComplex>) :
        ret.raw[2 * i] = real(z).value
        ret.raw[2 * i + 1] = imag(z).value
      (y) :
        fatal("Unhandled Type")
  return ret

But this snags on the match - it seems like it can't operate in the templated regime because ref<T> never matches on anything concrete.

My guess is that this is why the sizeof is failing as well. I see places in core.stanza where sizeof is used on deftype objects - specifically in allocate-initial-stack on Stack. So I think it must be something to do with the ref<T> again.

I ran into a similar roadblock with ref<T> when I tried to make functions such as:

public lostanza defn write-buf (C:ref<Tuple<Double>>, buf:ptr<double>, N:int) -> int :
  for (var i:int = 0, i < N, i = i + 1) :
    val x = C.items[i]
    buf[i] = x.value
  return 0

public lostanza defn write-buf (C:ref<Tuple<GComplex>>, buf:ptr<double>, N:int) -> int :
  for (var i:int = 0, i < N, i = i + 1) :
    val z = C.items[i]
    buf[2 * i] = z.x ; real(z).value
    buf[2 * i + 1] = z.y ; imag(z).value
  return 0

It throws errors like this:

src/GPolynomial.stanza:55.2: No appropriate function write-buf for arguments of type (ref<Tuple<T>>,
ptr<double>, int). Possibilities are:
  write-buf: (ref<Tuple<Double>>, ptr<double>, int) -> int at src/GPolynomial.stanza:31.21
  write-buf: (ref<Tuple<GComplex>>, ptr<double>, int) -> int at src/GPolynomial.stanza:37.21

OK - so I'll try something different:

public lostanza defn GPolynomial<Double> (C:ref<Tuple<Double>>) -> ref<GPolynomial<Double>> :
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  val sizeT = sizeof(double)

  val buf = call-c clib/malloc( N * sizeT ) as ptr<double>
  for (var i:int = 0, i < N, i = i + 1) :
    val x = C.items[i]
    buf[i] = x.value

  val ret = new GPolynomial<Double>{buf, N, C}
  return ret

This snags on a very strange error message:

$ stanza build gsl-tests
src/GPolynomial.stanza:74.13: Cannot access field value in expression of type ref<Double>.

It is erroring out on the line buf[i] = x.value. That is very strange to me because the .value seems to be the way to access ref<Double> in every other part of the code.

So - this must not be the way to write functions for parametric types - but it isn't clear why. The error message is not particularly helpful.

So finally - I back track again and implement this:

public lostanza defn GRealPolynomial (C:ref<Tuple<Double>>) -> ref<GPolynomial<Double>> :
  ; Create a Real value Coefficients Polynomial.
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  val sizeT = sizeof(double)

  val buf = call-c clib/malloc( N * sizeT ) as ptr<double>
  for (var i:int = 0, i < N, i = i + 1) :
    val x = C.items[i]
    buf[i] = x.value

  val ret = new GPolynomial<Double>{buf, N, C}
  return ret

Which works - it compiles and I can evaluate the generated real-value coeff polynomial with it.

I think this means I will also have to write a GComplexPolynomial that does something very similar except for complex value coeffs and returns ref<GPolynomial<GComplex>>.

OK - but I don't want to have duplicate code in GRealPolynomial and GComplexPolynomial - so I'll take one last stab at this. It seems like I would have to write some kind of defmulti write-buf for both the Tuple<Double> and Tuple<GComplex>. If this existed I could replace the for loop portion with a call to that function and then be able to use captured types.

So I make the following attempt:

public lostanza deftype GPolynomial<T> <: Unique & Lengthable :
  raw:ptr<double>
  size:int
  coeffs:ref<Tuple<T>>

; I use this type to inject the methods I need as
;  parametric types. 
deftype GPolyCoefficients<T> :
  Tuple<T> <: GPolyCoefficients<T>

defmulti elem-size<T> (C:Tuple<T>) -> Int
defmulti write-buf<T> (C:Tuple<T>, P:GPolynomial<T>) -> Int

lostanza defmethod elem-size<Double> (C:ref<Tuple<Double>>) -> ref<Int> :
  return new Int{sizeof(double) as int}

lostanza defmethod elem-size<GComplex> (C:ref<Tuple<GComplex>>) -> ref<Int> :
  return new Int{2 * sizeof(double) as int}

lostanza defmethod write-buf<Double> (C:ref<Tuple<Double>>, P:ref<GPolynomial<Double>>) -> ref<Int> :
  for (var i:int = 0, i < P.size, i = i + 1) :
    val x = C.items[i] as ref<Double>
    P.raw[i] = x.value
  return new Int{0}

lostanza defmethod write-buf<GComplex> (C:ref<Tuple<GComplex>>, P:ref<GPolynomial<GComplex>>) -> ref<Int> :
  for (var i:int = 0, i < P.size, i = i + 1) :
    val z = C.items[i] as ref<GComplex>
    P.raw[2 * i] = z.x ; real(z).value
    P.raw[2 * i + 1] = z.y ; imag(z).value
  return new Int{0}

public lostanza defn GPolynomial<?T> (C:ref<Tuple<?T>>) -> ref<GPolynomial<T>> :
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  val sizeT = elem-size<T>(C)

  val buf = call-c clib/malloc( N * sizeT.value ) as ptr<double>
  val ret = new GPolynomial<T>{buf, N, C}

  write-buf<T>(C, ret)

  return ret

This feels like it is really close - but the compiler still barfs with:

$> stanza build gsl-tests
src/GPolynomial.stanza:41.15: Cannot access field value in expression of type ref<Double>.
src/GPolynomial.stanza:47.19: Cannot access field x in expression of type ref<GComplex>.
src/GPolynomial.stanza:48.23: Cannot access field y in expression of type ref<GComplex>.

What is strange about this result is that It doesn't like it when I access the ref types in the C.items list, but it doesn't seem to mind that I access P.raw and P.size directly.

I tried removing the for loop and just access the first element of the tuple. That still throws the same error - so I don't think this is related to the for loop. Is this a restriction on the Tuple elements ? I'm not trying to write them - just read them.

I've pushed this last implementation here:

https://github.com/callendorph/lbstanza-gsl/tree/issues/new_polynomial

Thoughts ? Am I doing this wrong ? Do any of these feel like the right direction ?

Does the Cannot access field * in expression of ... error make sense to you ?

CuppoJava commented 1 year ago

Apologies for missing this issue.

The cannot access field xyz error message are indeed confusing. I'll dig in and figure out what is happening.

CuppoJava commented 1 year ago

Hi Carl,

I took a look through the code, and found the root cause. First, let's dissect those confusing error messages a bit. We need to improve these messages, but here is where they came from:

lostanza defmethod write-buf<Double> (C:ref<Tuple<Double>>, P:ref<GPolynomial<Double>>) -> ref<Int> :
  for (var i:int = 0, i < P.size, i = i + 1) :
    val x = C.items[i]
    P.raw[i] = x.value
  return new Int{0}

In the function above, the write-buf method actually takes one type argument called Double. So in the body of the function, Double actually refers to the type variable, and not the definition in core.

This is why you get the message:

Cannot access field value in expression of type ref<Double>.

Because Double is just an opaque type variable, and has no fields at all.

Other than the error message, the other issue is the use of the parametric types for this purpose. Stanza does not allow you to perform dispatch upon the "parametric" properties of any type.

E.g. Stanza will let you do a runtime dispatch between Int or String. But it won't let you do a runtime dispatch between Tuple<Int> or Tuple<String>. This is because in general, especially if users make heavy use of the ? type, Stanza doesn't know the contents of the collection.

In your specific case, you can use overloaded versions of the functions.

lostanza defn elem-size (C:ref<Tuple<Double>>) -> ref<Int> :
  return new Int{sizeof(double) as int}

lostanza defn elem-size (C:ref<Tuple<GComplex>>) -> ref<Int> :
  return new Int{2 * sizeof(double) as int}

lostanza defn write-buf (C:ref<Tuple<Double>>, P:ref<GPolynomial<Double>>) -> ref<Int> :
  for (var i:int = 0, i < P.size, i = i + 1) :
    val x = C.items[i]
    P.raw[i] = x.value
  return new Int{0}

lostanza defn write-buf (C:ref<Tuple<GComplex>>, P:ref<GPolynomial<GComplex>>) -> ref<Int> :
  for (var i:int = 0, i < P.size, i = i + 1) :
    val z = C.items[i]
    P.raw[2 * i] = z.x ; real(z).value
    P.raw[2 * i + 1] = z.y ; imag(z).value
  return new Int{0}

The above will just create four different functions, where two pairs happen to share the same name. Stanza will statically choose which one to call based upon the static types.

For your general constructor function you can type it out twice yourself:

public lostanza defn GPolynomial (C:ref<Tuple<Double>>) -> ref<GPolynomial<Double>> :
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  val sizeT = elem-size(C)

  val buf = call-c clib/malloc( N * sizeT.value ) as ptr<double>
  val ret = new GPolynomial<Double>{buf, N, C}

  write-buf(C, ret)

  return ret

public lostanza defn GPolynomial (C:ref<Tuple<GComplex>>) -> ref<GPolynomial<GComplex>> :
  val N:int = length(C).value
  if N <= 0 :
    throw(GSLException(gsl-EINVAL()))

  val sizeT = elem-size(C)

  val buf = call-c clib/malloc( N * sizeT.value ) as ptr<double>
  val ret = new GPolynomial<GComplex>{buf, N, C}

  write-buf(C, ret)

  return ret

Or you can also use the #for macro to do a copy-and-replace for you:

#for TYPE in (Double GComplex) :

  public lostanza defn GPolynomial (C:ref<Tuple<TYPE>>) -> ref<GPolynomial<TYPE>> :
    val N:int = length(C).value
    if N <= 0 :
      throw(GSLException(gsl-EINVAL()))

    val sizeT = elem-size(C)

    val buf = call-c clib/malloc( N * sizeT.value ) as ptr<double>
    val ret = new GPolynomial<TYPE>{buf, N, C}

    write-buf(C, ret)

    return ret

The parametric types are definitely the most difficult aspect of Stanza -- and for C++ programmers especially. Stanza's parametric types work closest to Java's generics. C++'s template system acts more like a sophisticated copy-and-replace system, rather than a typing feature, and so just has very different behaviour.

callendorph commented 1 year ago

Ya - so it seems like what I've done is just over complicate things. The key part seems to be to let the overloaded function types do the work. The parametric typing really only comes in to play for the definition of the GPolynomial type.

E.g. Stanza will let you do a runtime dispatch between Int or String. But it won't let you do a runtime dispatch between Tuple or Tuple. This is because in general, especially if users make heavy use of the ? type, Stanza doesn't know the contents of the collection.

Does this same logic apply for all collection types - like Array or Vector ? Or is it only specific to Tuple ?

Is the issue here that the type capture can only happen at one level ? ie, in some of my examples with the Tuple it is like trying to use the type capture at multiple levels, the Polynomial and the Tuple. So there is no way for the top-level to know what type got captured in the child Tuple type?

#for

OK - ya that's interesting. I'll try that out.

Thank you!

CuppoJava commented 1 year ago

Does this same logic apply for all collection types - like Array or Vector ? Or is it only specific to Tuple ?

It applies to all collection types and parametric types. This includes function types as well: there is no way for Stanza to perform a runtime-dispatch between:

Int -> Int

and

Int -> String

Is the issue here that the type capture can only happen at one level ? ie, in some of my examples with the Tuple it is like trying to use the type capture at multiple levels, the Polynomial and the Tuple. So there is no way for the top-level to know what type got captured in the child Tuple type?

That's pretty much correct. In the Stanza virtual machine all objects are tagged with a small integer that tells it what type of object it is. This tag says something like: "Int", or "Tuple", but it doesn't say anything about the contents. The runtime dispatch is only allowed to look at this tag to do its work.

Hope that makes it clearer! Patrick

callendorph commented 1 year ago

for TYPE in (Double GComplex) :

I think the syntax here is wrong. It must be:

#for TYPE in [Double GComplex] : 
...
OlegPliss commented 1 year ago

[] for a Tuple, () for a List.

CuppoJava commented 1 year ago

Sorry, yup your syntax is the right one:

#for TYPE in [Double GComplex] : 
...