-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial draft of the type system API #1
base: main
Are you sure you want to change the base?
Conversation
import { Type } from "./base"; | ||
|
||
export interface Indexer<T> extends Type<T> { | ||
readonly: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
readonly && writeonly? Maybe use an enum/string union
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would agree as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went through several iterations of points, going back & forth to compare what's written in the proposal vs. the API spec here. The goal of this feedback is to help align the API to the desired spec & intended breadth of the Typir library (focusing less on inference & checking, more on type system structural foundation stuff). Hopefully the feedback is helpful!
There are some questions as well, as I'm interpreting what the intended API will look like. I'm also definitely open for discussion on suggestions, questions, etc., but as is this is already looking quite interesting 👀 .
Here are some general points I would like to add, in addition to the more targeted feedback.
- A
list-type
type would be great to add, same asfunction-type
has been added already. - Feels like typeParams & args are duplicated throughout, I feel this can be factored out to just 'types'
- Options interfaces duplicate a lot of information, I would consider revising this, or at least refactoring in order to remove the overlap between options and the corresponding type/type constructor.
- Type constructor variance could be extended to apply more generally to type constructors (especially if List is added, this could also benefit from variance)
- I would consider adding a
TypeRule
interface. There's already several instances of type judgements which describe the form that rules can take (assignable, castable). Making it easier to define rules more generally would allow a greater degree of flexibility in the system design. We could still abstract away the finer details using this API, but all rules could then be changed if needed to customize behavior. Note this does not require adding a type checking system, but it would provide a nice form that a checker could then consume to perform inference & checking. - Adding to the point above, getting all types & rules (relationships) between types would be a nice feature to add to the type system foundation.
TypeSystem
is not entirely accurate for what this does, but it makes the most sense when describing what this will be used for. This almost feels like aTypeGraph
already, since we're defining nodes (types, categories, etc.), and then defining relationships on those nodes (edges). However, this naming can also lead to misconceptions, so it would be worthwhile to think about a name that indicates we're building a foundation for a type system to be built off of, rather than the complete type system itself.
readonly typeSystem: TypeSystem<T>; | ||
} | ||
|
||
export interface TypeMember<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Members appear to be annotated types, with name & optional in addition. This could extend Type, and supplement with additional properties to give it the same look & feel.
|
||
export interface Type<T> { | ||
readonly literal?: T; | ||
readonly members: Iterable<TypeMember<T>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows adding multiple members that share the same name. It might be easier to instead go with a map/record.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the order of members is important, isn't it?
readonly typeArguments: Type<T>[]; | ||
applyTypeArguments(args: Type<T>[]): FunctionType<T>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be nice to have at the level of all types, and not just functions. This would go along with the notion that a type may or may not be generic, but is still a type. The API could be retained like so, but it may help to then be able to invoke the same constraints on the param & return types.
readonly literal?: T; | ||
readonly type: Type<T>; | ||
readonly optional: boolean; | ||
readonly spread: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this feels like a specialized case of a function parameter. Internally this also places a constraint on the kinds of types this parameter can match with, feels like defining a typing rule before actually writing one. Not necessarily bad, but it may make it difficult to reason about typing rules/relationships later.
/** | ||
* Indicates that the last type in this tuple is spread. | ||
*/ | ||
spread: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A thought, do we really want to support the spread operator? I'm sure the case comes up, but is it often enough to really need to support it directly, rather than having it be implemented by devs when needed themselves. How often does this come up in languages that are expressed in Langium?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see support for the spread operator by Typir in general, but I am not sure, how to integrate it. Maybe as an add-on?
export interface PrimitiveType<T> extends Type<T> { | ||
readonly name: string | ||
readonly members: MemberCollection<T>; | ||
constant(options: PrimitiveTypeConstantOptions<T>): PrimitiveTypeConstant<T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, interesting, so does this imply types are capable of being modified in this system? If so, we're working with open types, which is more like interfaces than types, and will greatly complicate reasoning about them.
assignable(to: PrimitiveType<T>): Disposable; | ||
assignable(callback: AssignabilityCallback<PrimitiveType<T>>): Disposable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks nice from API perspective, but I'm curious how this would be implemented internally. Is the plan to have relationships for types stored on types, or higher up within the system itself. The latter would make it much easier to get information about all relationships/rules.
import type { TypeSystem } from "./type-system"; | ||
import { Disposable } from "./utils"; | ||
|
||
export interface TypeCategory<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little confused by the usage of a 'category' of things, as this is very general.
If this is being used to express classes, structs, and interfaces, why not provide a more direct representation to express classes, structs, and interfaces directly? Unless there's a concrete motivation to making a general type category, I would lean on keeping it simple and targeting known cases first. If it turns out we need something more general, we can always add it in later.
This would also be good considering the audience, which is likely not so versed in type theory, or type systems in general. Keeping to familiar terms & type forms will help users to leverage this library as intended, and effectively.
* | ||
* See [here](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) for an in-depth explanation. | ||
*/ | ||
export enum TypeParameterVariance { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type constructors in general can exhibit variance, not just parameters. Might be worthwhile considering this, especially if we add a generic List
type.
throw new Error('Not implemented'); | ||
} | ||
|
||
export interface TypeSystem<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question for the choice of naming TypeSystem
here. A complete type system would describe the types, judgement forms, rules, and logic for resolving types using these rules. This implementation allows defining types, and has some judgement forms already (assignable, castable), and will probably have a bit of logic there, but it's incomplete. There's no functionality to infer/typecheck expressions, which should be provided by the user. It's more of a structure for a system to be built on top of, maybe TypeSystemStructure
or TypeFoundation
would more clearly convey this aspect.
Can we see the proposal of what is being implemented somewhere? Or is it all in code only so far? |
@trusktr I've created an internal document that shows potential usage of this API before I started working on this. Note that the API has changed a bit while developing this PR, but the main ideas are still there. // A typed function that initializes a whole generic type system
// Every type has a `literal` that can be seen as its "source"
// We clarify that each literal is of type `AstNode`
const typeSystem = createTypeSystem<AstNode>();
// primitives
const stringType = typeSystem.primitive('string');
const numberType = typeSystem.primitive('number');
// Create an `any` type and set is as the top type of the type system
const anyType = typeSystem.primitive('any').top();
// operators
const plus = typeSystem.operator('+');
// the 'operate' function returns a `Disposable`
// In case we allow user-contributed operator overloads, these need to be deleted at some point
plus.operate({
operands: stringType,
result: stringType
});
plus.operate({
operands: [stringType, numberType],
order: 'flexible' // 'strict' would mean that number + string would not be valid here
result: stringType
});
//
plus.operate({
operands: [stringType, anyType],
result: stringType,
priority: -1
});
// classes, structs, interfaces
// These structures are classified as "categories"
const classType = typeSystem.category('class');
const interfaceType = typeSystem.category('interface');
const structType = typeSystem.category('struct');
classType.assignable(classType, (from, to) => {
// This is likely the default implementation for any complex type structure
// This will likely be exposed as `defaultNamedAssignability`
if (from.literal === to.literal) {
return true;
}
if (from.super.some(e => typeSystem.isAssignable(e, to)) {
return true;
}
return false;
});
classType.assignable(interfaceType, defaultNamedAssignability);
structType.assignable(structType, defaultNamedAssignability);
structType.assignable(interfaceType, defaultNamedAssignability);
interfaceType.assignable(interfaceType, defaultNamedAssignability);
// In addition, the library also exports a `defaultStructuralAssignability` function
// This allows for example for a fully structural type system (such as TypeScript) or a partially structural type system
// Such a type system can for example use named assignability for classes, while using structural assignability for interfaces
// These types can then be specialized to create their class/struct/interface instances
const classInstance = classType.get(classLiteral);
// `classInstance` is an disposable instance of the type system.
// It features the same API and can be used to implement more complex type behavior
// This is an example of a user defined implicit casting function
// Like most things, it returns a disposable
classInstance.assignable((from, to) => {
return typeSystem.isAssignable(implicitCastingType, to);
});
// Example of a user defined explicit casting function
classInstance.castable((from, to) => {
return typeSystem.isAssignable(explicitCastingLiteral, to);
});
// Complex type instances can also be generic
// We first have to define the type parameters
const genericSimpleType = typeSystem.typeParameter({
name: 'T';
});
const genericConstraintType = typeSystem.typeParameter({
name: 'T2',
// A constraint if effectively just a function with (Type) => boolean
// The type system object provides a few default constraints (such as `extends`)
contraints: [
typeSystem.extends(someClassLiteral)
]
});
const genericInstance = classType.get({
literal: classLiteral
parameters: [genericSimpleType, genericConstraintType]
});
// The generic types can be accessed using the `genericInstance`
const typeParameters = genericInstance.typeParameters
// A generic class can be specialized with type instances
const genericInstanceWithArguments = genericInstance.typeArguments([
T1Instance,
T2Instance
});
// Language validators need to assert that T2Instance fits the constraints
typeSystem.assertConstraints(typeParameters[1], T2Instance);
// All types can have members
// push also returns a disposable
// This allows to declare partial classes or extension functions/properties
// And also remove them on an update of their declaring document/class
classInstance.members.push(...members);
// Delegate types
const delegateInstance = typeSystem.delegate({
// names are usually optional throughout the whole type system
// The type system needs to support inline types
name: '...',
literal: delegateLiteral,
parameters: [],
returnType: ...,
generics: []
});
// Union types
const unionInstance = typeSystem.union({
literal: unionLiteral,
generics: [ /** can also support generics */ ]
}); |
Here are my general thoughts about this API proposal:
Some more things we probably need:
|
For anyone who wants to see how this API is supposed to look like. I will accept feedback on the API direction and then start implementing the library.