Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d37784b
first working version for defining simple custom types
JohannesMeierSE Mar 19, 2025
edd4a32
fixed some other smaller issues
JohannesMeierSE Mar 25, 2025
fa9e3b5
fixed generics, small issues
JohannesMeierSE Apr 4, 2025
9252fe4
no strings for TypeSelectors
JohannesMeierSE Apr 4, 2025
44f5b37
calculate identifiers only from custom properties
JohannesMeierSE Apr 4, 2025
f98b7c7
tests for custom types depending on custom types, no strings for Type…
JohannesMeierSE Apr 4, 2025
bedb16c
test cases for type inference
JohannesMeierSE Apr 6, 2025
f79d0df
fixed bug
JohannesMeierSE Apr 11, 2025
53b3ad3
register as listener and get informed about all existing types, if de…
JohannesMeierSE Apr 11, 2025
8b60ada
conversion and sub-types for custom types
JohannesMeierSE Apr 11, 2025
bdbe76a
test case for validation rules attached to custom type-specific infer…
JohannesMeierSE Apr 11, 2025
796630e
specify names and user representations once for all custom properties…
JohannesMeierSE Apr 11, 2025
0c4659d
test multiple custom types in parallel
JohannesMeierSE Apr 14, 2025
1ced5ca
fixed eslint rule
JohannesMeierSE Apr 15, 2025
74a6f90
support nesting of Properties for custom types
JohannesMeierSE Apr 15, 2025
2571d82
new property to skip type-specific inference rules for types which al…
JohannesMeierSE Apr 16, 2025
7b94761
removed testing stuff, fixed imports
JohannesMeierSE Apr 16, 2025
4e9e31f
more test cases for TypeSelectors with custom types
JohannesMeierSE Apr 16, 2025
d9ea8e1
first improvements according to the review
JohannesMeierSE Jul 4, 2025
98ec8e9
more improvements
JohannesMeierSE Jul 5, 2025
e50d304
improved comments according to the review
JohannesMeierSE Jul 8, 2025
1f63167
properties of custom types are readonly now, default implementation f…
JohannesMeierSE Jul 9, 2025
7003a61
added missing implementation (which will be improved in future PRs)
JohannesMeierSE Jul 9, 2025
7a98ce1
unified initialization design for classes, functions and custom types
JohannesMeierSE Jul 9, 2025
67de361
check typeName for uniqueness
JohannesMeierSE Jul 9, 2025
e551485
wrote some documentation for custom types
JohannesMeierSE Jul 9, 2025
6567a1e
fixes according to the review
JohannesMeierSE Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/typir/src/initialization/type-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TypeReference } from './type-reference.js';
// TODO find better names: TypeSpecification, TypeDesignation/Designator, ... ?
export type BasicTypeSelector<T extends Type, LanguageType> =
| T // the wanted type
| string // identifier of the type (to be searched in the type graph/map); TODO ist das in der Praxis wirklich nützlich? ruft man nicht einfach nur Kind.get(...) auf (also Type | TypeReference | undefined)?
| string // identifier of the type (to be searched in the type graph/map)
| TypeInitializer<T, LanguageType> // delayed creation of types
| TypeReference<T, LanguageType> // reference to a (maybe delayed) type
| LanguageType // language node to infer the final type from
Expand Down
7 changes: 5 additions & 2 deletions packages/typir/src/kinds/custom/custom-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ export type CustomTypePropertyTypes =
export type TypeSelectorForCustomTypes<T extends Type, LanguageType> = Exclude<TypeSelector<T, LanguageType>, string>;

export type CustomTypePropertyInitialization<T extends CustomTypePropertyTypes, LanguageType> =
// replace Type by a TypeSelector for it ...
T extends Type ? TypeSelectorForCustomTypes<T, LanguageType> : // note that TypeSelector includes "unknown" (if the LanguageType is not specified), which makes the TypeScript type-checking "useless" here!
/* replace Type by a TypeSelector for it ...
* (Note this special case: If the LanguageType is set to "unknown", then the TypeSelector includes "unknown",
* which makes the TypeScript type-checking "useless" here, i.e. the TypeScript compiler allows you to use any value here (e.g. 'true') which does not work in general!
* Therefore "unknown" should not be used for LanguageType if possible.) */
T extends Type ? TypeSelectorForCustomTypes<T, LanguageType> :
// unchanged for the atomic cases:
T extends (string | number | boolean | bigint | symbol) ? T :
// ... in recursive way for the composites:
Expand Down
3 changes: 2 additions & 1 deletion packages/typir/src/kinds/custom/custom-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export interface CustomKindOptions<Properties extends CustomTypeProperties, Lang
/** Name for this custom kind. */
name: string;

/** This identifier needs to consider all properties which make the custom type unique. The identifiers are used to detect unique custom types. */
/** This identifier needs to consider all properties which make the custom type unique. The identifiers are used to detect unique custom types.
* It is the responsibility of the user of Typir to consider all relevant properties and their structure/nesting. */
calculateTypeIdentifier: (properties: CustomTypeInitialization<Properties, LanguageType>) => string;

/** Define the name for each custom type; might be overridden by the custom type-specific name. */
Expand Down
24 changes: 8 additions & 16 deletions packages/typir/src/kinds/custom/custom-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class CustomType<Properties extends CustomTypeProperties, LanguageType> e
this.typeUserRepresentation = typeDetails.typeUserRepresentation;

const collectedReferences: Array<TypeReference<Type, LanguageType>> = [];
this.properties = this.replaceWhole(typeDetails.properties, collectedReferences) as CustomTypeStorage<Properties, LanguageType>;
this.properties = this.replaceAllProperties(typeDetails.properties, collectedReferences) as CustomTypeStorage<Properties, LanguageType>;
const allReferences: Array<TypeReference<Type, unknown>> = collectedReferences as Array<TypeReference<Type, unknown>>; // type-node.ts does not use <LanguageType>

this.defineTheInitializationProcessOfThisType({
Expand All @@ -42,24 +42,16 @@ export class CustomType<Properties extends CustomTypeProperties, LanguageType> e
}); // TODO Are there more preconditions? deregistration from listeners?
}

protected replaceWhole(properties: CustomTypeInitialization<CustomTypeProperties, LanguageType>, collectedReferences: Array<TypeReference<Type, LanguageType>>): CustomTypeStorage<CustomTypeProperties, LanguageType> {
protected replaceAllProperties(properties: CustomTypeInitialization<CustomTypeProperties, LanguageType>, collectedReferences: Array<TypeReference<Type, LanguageType>>): CustomTypeStorage<CustomTypeProperties, LanguageType> {
const result: CustomTypeStorage<CustomTypeProperties, LanguageType> = {};
for (const [key, value] of Object.entries(properties)) {
const transformed: CustomTypePropertyStorage<CustomTypePropertyTypes, LanguageType> = this.replace(value, collectedReferences);
const transformed: CustomTypePropertyStorage<CustomTypePropertyTypes, LanguageType> = this.replaceSingleProperty(value, collectedReferences);
result[key] = transformed;
}
return result;
// for (const key in properties) {
// if (Object.prototype.hasOwnProperty.call(properties, key)) { // https://eslint.org/docs/latest/rules/guard-for-in
// const value = properties[key];
// const transformed: CustomTypePropertyStorage<CustomTypePropertyTypes, LanguageType> = this.replace(value, collectedReferences);
// result[key] = transformed as CustomTypePropertyStorage<CustomTypePropertyTypes, LanguageType>;
// }
// }
// return result;
}

protected replace<T extends CustomTypePropertyTypes>(value: CustomTypePropertyInitialization<T, LanguageType>, collectedReferences: Array<TypeReference<Type, LanguageType>>): CustomTypePropertyStorage<T, LanguageType> {
protected replaceSingleProperty<T extends CustomTypePropertyTypes>(value: CustomTypePropertyInitialization<T, LanguageType>, collectedReferences: Array<TypeReference<Type, LanguageType>>): CustomTypePropertyStorage<T, LanguageType> {
// TypeSelector --> TypeReference
// function
// Type
Expand Down Expand Up @@ -104,16 +96,16 @@ export class CustomType<Properties extends CustomTypeProperties, LanguageType> e
}
// grouping with Array, Set, Map
else if (Array.isArray(value)) {
return value.map(content => this.replace(content, collectedReferences)) as unknown as CustomTypePropertyStorage<T, LanguageType>;
return value.map(content => this.replaceSingleProperty(content, collectedReferences)) as unknown as CustomTypePropertyStorage<T, LanguageType>;
} else if (isSet(value)) {
const result = new Set<CustomTypePropertyStorage<T, LanguageType>>();
for (const entry of value) {
result.add(this.replace(entry, collectedReferences));
result.add(this.replaceSingleProperty(entry, collectedReferences));
}
return result as unknown as CustomTypePropertyStorage<T, LanguageType>;
} else if (isMap(value)) {
const result: Map<string, CustomTypePropertyStorage<T, LanguageType>> = new Map();
value.forEach((content, key) => result.set(key, this.replace(content, collectedReferences)));
value.forEach((content, key) => result.set(key, this.replaceSingleProperty(content, collectedReferences)));
return result as unknown as CustomTypePropertyStorage<T, LanguageType>;
}
// primitives
Expand All @@ -122,7 +114,7 @@ export class CustomType<Properties extends CustomTypeProperties, LanguageType> e
}
// composite with recursive object / index signature
else if (typeof value === 'object' && value !== null) {
return this.replaceWhole(value as CustomTypeInitialization<CustomTypeProperties, LanguageType>, collectedReferences) as CustomTypePropertyStorage<T, LanguageType>;
return this.replaceAllProperties(value as CustomTypeInitialization<CustomTypeProperties, LanguageType>, collectedReferences) as CustomTypePropertyStorage<T, LanguageType>;
} else {
throw new Error(`missing implementation for ${value}`);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/typir/test/kinds/custom/custom-cycles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js
import { TypirServices } from '../../../src/typir.js';
import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js';

// These test cases test, that custom types might depend on other types including custom types
// and the creation of custom types is delayed, when those types are not yet existing.

export type MyCustomType = {
dependsOnType: Type; // PrimitiveType | CustomType<MyCustomType, TestLanguageNode>; TODO extends
myProperty: number;
Expand Down
27 changes: 21 additions & 6 deletions packages/typir/test/kinds/custom/custom-example-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { RuleRegistry } from '../../../src/utils/rule-registration.js';
import { createTypirServicesForTesting, expectToBeType, expectTypirTypes, expectValidationIssuesNone, expectValidationIssuesStrict } from '../../../src/utils/test-utils.js';
import { assertTypirType } from '../../../src/utils/utils.js';

/**
* The custom type called "Matrix" represents a two-dimensional array of primitive types.
* Known from mathematics, the "width" represents the number of columns and the "height" the number of row of a matrix.
*
* This TypeScript type specifies the properties of the the Typir types which represent "matrices".
*/
export type MatrixType = { // "interface" instead of "type" does not work!
baseType: PrimitiveType;
width: number;
Expand All @@ -32,7 +38,8 @@ describe('Tests simple custom types for Matrix types', () => {
// create a custom kind to create custom types with dedicated properties, as defined in <MatrixType>
const customKind = new CustomKind<MatrixType, TestLanguageNode>(typir, {
name: 'Matrix',
// determine which identifier is used to store and retrieve a custom type in the type graph (and to check its uniqueness)
// determine which identifier is used to store and retrieve a custom type in the type graph
// (and to check its uniqueness, i.e. if two types have the same identifier, they are the same and only one of it will be added to the type graph)
calculateTypeIdentifier: properties =>
`custom-matrix-${typir.infrastructure.TypeResolver.resolve(properties.baseType).getIdentifier()}-${properties.width}-${properties.height}`,
});
Expand Down Expand Up @@ -144,8 +151,8 @@ describe('Tests simple custom types for Matrix types', () => {
// ... but a single, generic inference rule here
typir.Inference.addInferenceRule(node => {
if (node instanceof MatrixLiteral) {
const width = node.elements.length;
const height = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0);
const width = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0); // the number of cells in the longest row
const height = node.elements.length; // the number of rows
const type = customKind.get({ baseType: integerType, width, height });
return type.getType() || InferenceRuleNotApplicable;
}
Expand Down Expand Up @@ -175,8 +182,8 @@ describe('Tests simple custom types for Matrix types', () => {
// a single, generic inference rule
typir.Inference.addInferenceRule(node => {
if (node instanceof MatrixLiteral) {
const width = node.elements.length;
const height = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0);
const width = node.elements.map(row => row.length).reduce((l, r) => Math.max(l, r), 0); // the number of cells in the longest row
const height = node.elements.length; // the number of rows
return customKind.create({ typeName: 'My1x1MatrixType', properties: { baseType: integerType, width, height }})
.finish().getTypeFinal()!; // we know, that the type can be created now, without delay
}
Expand Down Expand Up @@ -258,7 +265,7 @@ describe('Tests simple custom types for Matrix types', () => {
expect(countInferenceRules()).toBe(initialInferenceRuleSize + 1); // new Matrix type with its own inference rule
// "create it again" => in the end, the existing Matrix type is reused
const matrix2x2Another = getOrCreateMatrixType(2, 2, false);
// both types are the same, ...
// both types are the same (since they have the same identifier, since it contains the same values for the primitive type, width and height), ...
expect(matrix2x2).toBe(matrix2x2Another);
Comment thread
insafuhrmann marked this conversation as resolved.
// ... but we have another inference rule now, since the rules for the new type are moved to the existing type!
expect(countInferenceRules()).toBe(initialInferenceRuleSize + 2);
Expand All @@ -282,12 +289,20 @@ describe('Tests simple custom types for Matrix types', () => {
});


/**
* Instances of this class represent literals for matrices in the AST, i.e. AST nodes. An example might be visualized like this:
* [ 1, 2, 3;
* 4, 5, 6 ]
* They are similar to array literals in usual programming languages.
*/
class MatrixLiteral extends TestExpressionNode {
constructor(
public elements: IntegerLiteral[][],
) { super(); }
}

// some predefined literals for matrices to be reused in test cases

const matrixLiteral1x1 = new MatrixLiteral([
[new IntegerLiteral(1)],
]);
Expand Down
21 changes: 21 additions & 0 deletions packages/typir/test/kinds/custom/custom-example-restricted.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { IntegerLiteral, TestExpressionNode, TestLanguageNode } from '../../../s
import { TypirServices } from '../../../src/typir.js';
import { createTypirServicesForTesting, expectToBeType } from '../../../src/utils/test-utils.js';

/**
* The custom type called "RestrictedInteger" represents a primitive integer type with an upper bound,
* i.e. it looks like usual integers, but integer values/literals higher than the upper bound cannot be assigned to the restricted integer.
*
* This TypeScript type specifies the properties of the the Typir types which represent "restricted integers".
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated
*/
export type RestrictedInteger = {
upperBound: number;
};
Expand All @@ -30,6 +36,8 @@ describe('Tests inference and assignability for Integers with an upper bound', (

customKind = new CustomKind<RestrictedInteger, TestLanguageNode>(typir, {
name: 'RestrictedInteger',
// determine which identifier is used to store and retrieve a custom type in the type graph
// (and to check its uniqueness, i.e. if two types have the same identifier, they are the same and only one of it will be added to the type graph)
calculateTypeIdentifier: properties => `custom-restricted-integer-${properties.upperBound}`,
calculateTypeName: properties => `RI-${properties.upperBound}`, // the name for each RestrictedInteger type
// each RestrictedIntegerType is an IntegerType!
Expand All @@ -52,6 +60,11 @@ describe('Tests inference and assignability for Integers with an upper bound', (
});
});

/**
* Utility function to create a new restricted type in the Typir type system.
* @param upperBound the desired upper bound
* @returns the restricted type which is part of the type system in the current Typir instance/services
*/
function restrictedType(upperBound: number): CustomType<RestrictedInteger, TestLanguageNode> {
return customKind.create({ properties: { upperBound } }).finish().getTypeFinal()!;
}
Expand Down Expand Up @@ -116,12 +129,20 @@ describe('Tests inference and assignability for Integers with an upper bound', (
});


/**
* Instances of this class represent literals of restricted integers in the AST, i.e. AST nodes.
* In other words, RestrictedIntegerLiteral is for restricted integers, what IntegerLiteral (see predefined-language-nodes.ts) is for integers.
*
* While literals dedicated for restricted integers usually not occur in practical applications (only IntegerLiterals usually occur),
* they are used here for demonstration and to make test cases shorter.
*/
class RestrictedIntegerLiteral extends TestExpressionNode {
Comment thread
insafuhrmann marked this conversation as resolved.
constructor(
public value: number,
public upperBound: number,
) { super(); }
}

// some predefined literals for restricted integers to be reused in test cases
const int2Limit10 = new RestrictedIntegerLiteral(2, 10);
const int6Limit10 = new RestrictedIntegerLiteral(6, 10);
3 changes: 3 additions & 0 deletions packages/typir/test/kinds/custom/custom-independent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js
import { TypirServices } from '../../../src/typir.js';
import { createTypirServicesForTesting, expectTypirTypes } from '../../../src/utils/test-utils.js';

Comment thread
insafuhrmann marked this conversation as resolved.
// These test cases test, that it is possible to work with two different kinds of custom types independent from each other in the same Typir instance,
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated
// even when these custom types/kinds have the same properties!

export type MyCustomType1 = {
myNumber: number;
myString: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TestLanguageNode } from '../../../src/test/predefined-language-nodes.js
import { TypirServices } from '../../../src/typir.js';
import { createTypirServicesForTesting } from '../../../src/utils/test-utils.js';

// These test cases test, that nesting of properties for custom types is possible (including recursion).
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated

export type NestedProperty = {
myBool: boolean;
myType: PrimitiveType;
Expand Down
2 changes: 2 additions & 0 deletions packages/typir/test/kinds/custom/custom-selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { TestExpressionNode, TestLanguageNode } from '../../../src/test/predefin
import { TypirServices } from '../../../src/typir.js';
import { createTypirServicesForTesting } from '../../../src/utils/test-utils.js';

// These test cases test, that all possible TypeSelectors work for custom types.
Comment thread
JohannesMeierSE marked this conversation as resolved.
Outdated

export type MyCustomProperties = {
dependsOnType?: CustomType<MyCustomProperties, TestLanguageNode>;
myProperty: number;
Expand Down