pkl-community / pkl-typescript

TypeScript language bindings for the Pkl programming language
https://www.npmjs.com/package/@pkl-community/pkl-typescript
Apache License 2.0
105 stars 1 forks source link

A weird issue with "Cannot generate type X as TypeScript." #33

Open rubythulhu opened 7 months ago

rubythulhu commented 7 months ago

I get the following (confusing!) error. Code is below:

Error: –– Pkl Error ––
Cannot generate type TransformInfuseRule as TypeScript.

58 | else throw("Cannot generate type \(type.referent.name) as TypeScript.")
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.typescript.internal.typegen#generateDeclaredType.<function#3> (file:///Users/rubiana/work/ZeroSpace/gg/pkl-bindings-tests/pkl-typescript/dist/codegen/src/internal/typegen.pkl)

44 | let (mapped = seenMappings.findOrNull((it) -> it.source == referent))
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.typescript.internal.typegen#generateDeclaredType.<function#2> (file:///Users/rubiana/work/ZeroSpace/gg/pkl-bindings-tests/pkl-typescript/dist/codegen/src/internal/typegen.pkl)

This causes the error:

module infuse

class StatMultiplier { fixed type="mult"; num: Number }
class StatReplacer { fixed type="replace"; num: Number }
class StatAdder { fixed type = "add"; num: Number }

typealias InfuseStat = StatMultiplier | StatReplacer | StatAdder

class TransformInfuseRule {
  fixed type = "transform"
  into: String
}

class BasicInfuseRule {
  fixed type = "basic"
  supply: InfuseStat?
  hp: InfuseStat?
  damage: InfuseStat?
  reload_time: InfuseStat?
  cooldown: InfuseStat?
}

fixed standard_infuse_rule = new BasicInfuseRule {
  supply = new StatMultiplier { num = 2.0 }
  hp = new StatMultiplier { num = 2.0 }
  cooldown = new StatMultiplier  { num = 0.5 }
}

fixed damage_infuse_rule = new BasicInfuseRule {
  supply = new StatMultiplier  { num = 2.0 }
  hp = new StatMultiplier { num = 2.0 }
  damage = new StatMultiplier { num = 2.0 }
}

typealias InfuseRule = TransformInfuseRule | BasicInfuseRule

function transform (n: String) = new TransformInfuseRule { into = n }

This works:

module infuse

class StatMultiplier { fixed type="mult"; num: Number }
class StatReplacer { fixed type="replace"; num: Number }
class StatAdder { fixed type = "add"; num: Number }

typealias InfuseStat = StatMultiplier | StatReplacer | StatAdder

// class TransformInfuseRule {
//   type = "transform"
//   into: String
// }

class InfuseRule {
  type: "basic"|"transform"
  supply: InfuseStat?
  hp: InfuseStat?
  damage: InfuseStat?
  reload_time: InfuseStat?
  cooldown: InfuseStat?
  into: String?
}

fixed standard_infuse_rule = new InfuseRule {
  type = "basic"
  supply = new StatMultiplier { num = 2.0 }
  hp = new StatMultiplier { num = 2.0 }
  cooldown = new StatMultiplier  { num = 0.5 }
}

fixed damage_infuse_rule = new InfuseRule {
  type = "basic"
  supply = new StatMultiplier  { num = 2.0 }
  hp = new StatMultiplier { num = 2.0 }
  damage = new StatMultiplier { num = 2.0 }
}

// typealias InfuseRule = TransformInfuseRule | BasicInfuseRule

// function transform (n: String) = new TransformInfuseRule { into = n }
function transform (n: String) = new InfuseRule { type = "transform"; into = n }

The interesting bit is there's no problem with InfuseStat, just InfuseRule, and the only major diff i can see is that the types inside InfuseStat all have the same shape, but the rules have different keys...

jasongwartz commented 7 months ago

We can discuss it more on slack, but the first example does successfully codegen for me, working with a pkl-gen-typescript from latest main branch, built using npm run build and executed via ./dist/bin/pkl-gen-typescript. Here's the output:

// This file was generated by `pkl-typescript` from Pkl module `infuse`.
// DO NOT EDIT.
import * as pklTypescript from "@pkl-community/pkl-typescript"

// Ref: Module root.
export interface Infuse {
  standardInfuseRule: pklTypescript.Any

  damageInfuseRule: pklTypescript.Any
}

// Ref: Pkl class `infuse.StatMultiplier`.
export interface StatMultiplier {
  type: pklTypescript.Any

  num: number
}

// Ref: Pkl class `infuse.StatReplacer`.
export interface StatReplacer {
  type: pklTypescript.Any

  num: number
}

// Ref: Pkl class `infuse.StatAdder`.
export interface StatAdder {
  type: pklTypescript.Any

  num: number
}

// Ref: Pkl class `infuse.TransformInfuseRule`.
export interface TransformInfuseRule {
  type: pklTypescript.Any

  into: string
}

// Ref: Pkl class `infuse.BasicInfuseRule`.
export interface BasicInfuseRule {
  type: pklTypescript.Any

  supply: InfuseStat|null

  hp: InfuseStat|null

  damage: InfuseStat|null

  reloadTime: InfuseStat|null

  cooldown: InfuseStat|null
}

// Ref: Pkl type `infuse.InfuseStat`.
type InfuseStat = StatMultiplier | StatReplacer | StatAdder

// Ref: Pkl type `infuse.InfuseRule`.
type InfuseRule = TransformInfuseRule | BasicInfuseRule

// LoadFromPath loads the pkl module at the given path and evaluates it into a Infuse
export const loadFromPath = async (path: string): Promise<Infuse> => {
  const evaluator = await pklTypescript.newEvaluator(pklTypescript.PreconfiguredOptions);
  try {
    const result = await load(evaluator, pklTypescript.FileSource(path));
    return result
  } finally {
    evaluator.close()
  }
};

export const load = (evaluator: pklTypescript.Evaluator, source: pklTypescript.ModuleSource): Promise<Infuse> =>
  evaluator.evaluateModule(source) as Promise<Infuse>;
jasongwartz commented 7 months ago

Separately from the bug report, some notes on your Pkl code:

rubythulhu commented 7 months ago

Separately from the bug report, some notes on your Pkl code:

  • You'll notice that properties defined like fixed type = "transform" are coming out of codegen as type: pklTypescript.Any. That's because there is a semantic difference in Pkl between type = "transform" and type: String = "transform" - the former is actually of Any type with a value set to the string literal.
  • In cases like these, where you want it to be a specific literal value, you might get better results using type: "transform" (ie. a string literal type) as opposed to a fixed property. That also has the benefit of producing generated typescript that also is defined as a string literal type
  • it looks like with your classes up top, like class StatMultiplier { fixed type="mult"; num: Number }, you're using type as a sort of discriminated union - where you can use type to figure out the class type. Again, you're limited by the variables being typed as Any. If you use literal types, and get a TypeScript string literal type in the output, you can write TypeScript code that does stuff like if (type === "mult") which has nice benefits: a) the string you're checking will be autocompleted in your editor, and b) TypeScript will do "type narrowing" based on the comparison you do. I just added an example of this to the tests - in that example, if I had a variable myFruit of type Fruit, and did a if (myFruit.type === "apple") { ... }, TypeScript would allow me to safely access myFruit.sweetness inside the conditional block

I've been so confused about the Anys, thank you! I'll clean those up as that was exactly the effect i was looking for, i just didn't realize that was the difference between fixed foo = "blah" and foo: "blah", and i did assume that the foo = form used an inferred type and not just Any, too used to type inference being all over the place nowadays.