langium icon indicating copy to clipboard operation
langium copied to clipboard

Incompatible ts property types when grammar mixes properties and unassigned rule calls

Open dgDSA opened this issue 10 months ago • 2 comments

Langium version: 3.3.1 Package name: https://registry.npmjs.org/langium/-/langium-3.3.1.tgz

Synopsis: If a non-terminal has an alternative of a keyword property and an unassigned rule call, and the unassigned rule call refers to a non-terminal with the same keyword property, then the .ts code won't compile.

Context: We need to represent the type system of our domain in our Langium grammar. Properly representing the subtype hierarchy is important for our application. Simplified example: example.langium.txt

Generating the attached example.langium yields typescript modules with syntax errors:

Interface 'BaseNumberType' incorrectly extends interface 'BasePrimitiveType'.
  Types of property 'typeStr' are incompatible.
    Type '"base:Float" | "base:Int"' is not assignable to type '"base:Bool" | "base:String"'.
      Type '"base:Float"' is not assignable to type '"base:Bool" | "base:String"'.

Expected behavior: The generated TS code should be compilable. One possible approach for this would be to let the string literal narrowing include the values from unassigned rule calls (as it already does for $type), e.g.:

export interface BasePrimitiveType extends AstNode {
    readonly $type: 'BaseNumberType' | 'BasePrimitiveType';
    typeStr: 'base:Bool' | 'base:String' | 'base:Float' | 'base:Int';
}

As a workaround, we tried adding "infers" to our grammar: example_with_infers.langium.txt But for longer rule call chains, this doesn't work either:

Interface 'BaseExceptionType' incorrectly extends interface 'BaseComplexType'.
  Types of property 'typeStr' are incompatible.
    Type '"io:FileNotFoundException" | "io:FlashPermissionException"' is not assignable to type '"base:Exception" | "base:List" | "base:Map" | "base:Semaphore" | "base:TypeException" | "io:File"'.

dgDSA avatar Mar 14 '25 08:03 dgDSA

Hey @dgDSA,

Have you tried using declared types as a workaround?

We need to represent the type system of our domain in our Langium grammar.

On a general note: I wouldn't recommend doing this (seen it a lot, doesn't turn out well), but instead add structures to the language that allow you to represent the type system within the DSL. For example in a builtin library.

msujew avatar Mar 14 '25 09:03 msujew

Hi @msujew,

thanks for your suggestion. Indeed, I was able to resolve our problem by declaring types, and by letting all of our non-terminals return the correct type, e.g.:

interface DataType {
    typeStr: string;
}

BasePrimitiveType returns DataType:
    BaseNumberType | typeStr=('base:Bool' | 'base:String');

BaseNumberType returns DataType:
    typeStr=('base:Int' | 'base:Float');

And actually this also brings our TypeScript development to a better level of abstraction.

Even though this issue is no longer pressing for us, I would recommend leaving this issue open, as other users will certainly also have the expectation that a correct .langium file should result in valid .ts code.

Also, thank you for pointing us to builtin libraries, we're going to take a look at that.

dgDSA avatar Mar 14 '25 14:03 dgDSA