Skip to content

Commit

Permalink
Stricter condition for type assignability (#2902)
Browse files Browse the repository at this point in the history
  • Loading branch information
RunDevelopment authored May 22, 2024
1 parent 8d11047 commit 9f3e2b2
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 10 deletions.
93 changes: 93 additions & 0 deletions src/common/types/assign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {
NeverType,
NonNeverType,
NumberPrimitive,
StringPrimitive,
Type,
UnionType,
intersect,
isDisjointWith,
} from '@chainner/navi';

export type AssignmentResult = AssignmentOk | AssignmentError;
export interface AssignmentOk {
readonly isOk: true;
readonly assignedType: NonNeverType;
}
export interface AssignmentError {
readonly isOk: false;
readonly assignedType: NeverType;
readonly errorType: Type;
}

/**
* We only want to split if there is at least one specific struct instance or there are no struct instances at all.
*/
const shouldSplit = (items: readonly Type[]): boolean => {
let noStructs = true;
for (const item of items) {
if (item.underlying === 'struct') {
noStructs = false;
if (item.type === 'instance') {
return true;
}
}
}
return noStructs;
};
const splitType = (t: Type): Type[] => {
if (t.underlying === 'union' && shouldSplit(t.items)) {
const numbers: NumberPrimitive[] = [];
const strings: StringPrimitive[] = [];
const result: Type[] = [];
for (const item of t.items) {
if (item.underlying === 'number') {
numbers.push(item);
} else if (item.underlying === 'string') {
strings.push(item);
} else {
result.push(item);
}
}

if (numbers.length === 1) {
result.push(numbers[0]);
} else if (numbers.length >= 2) {
result.push(new UnionType(numbers as never));
}
if (strings.length === 1) {
result.push(strings[0]);
} else if (strings.length >= 2) {
result.push(new UnionType(strings as never));
}
return result;
}
return [t];
};

/**
* Returns whether the type `t` can be assigned to an input of type `definitionType`.
*/
export const assign = (t: Type, definitionType: Type): AssignmentResult => {
const split = splitType(t);
if (split.length > 1) {
for (const item of split) {
if (isDisjointWith(item, definitionType)) {
return { isOk: false, assignedType: NeverType.instance, errorType: item };
}
}
}

const intersection = intersect(t, definitionType);
if (intersection.underlying === 'never') {
return { isOk: false, assignedType: intersection, errorType: t };
}
return { isOk: true, assignedType: intersection };
};

/**
* Equivalent to `assign(t, definitionType).isOk`, but faster.
*/
export const assignOk = (t: Type, definitionType: Type): boolean => {
return assign(t, definitionType).isOk;
};
18 changes: 9 additions & 9 deletions src/common/types/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
evaluate,
getReferences,
intersect,
isDisjointWith,
literal,
union,
without,
} from '@chainner/navi';
import { Input, InputId, InputSchemaValue, NodeSchema, Output, OutputId } from '../common-types';
import { EMPTY_MAP, assertNever, lazy, lazyKeyed, topologicalSort } from '../util';
import { assign, assignOk } from './assign';
import { getChainnerScope } from './chainner-scope';
import { fromJson } from './json';
import type { PassthroughInfo } from '../PassthroughMap';
Expand Down Expand Up @@ -454,7 +454,7 @@ export class FunctionDefinition {
this.inputConvertibleDefaults = new Map(
[...this.inputDefaults].map(([id, d]) => {
const c = this.inputConversions.get(id)?.convertibleTypes ?? NeverType.instance;
return [id, union(d, c)];
return [id, union(d, c)] as const;
})
);

Expand Down Expand Up @@ -492,11 +492,11 @@ export class FunctionDefinition {
}

canAssignInput(inputId: InputId, type: Type): boolean {
const inputType = this.inputDefaults.get(inputId);
if (!inputType) {
const definitionType = this.inputDefaults.get(inputId);
if (!definitionType) {
throw new Error('Invalid input id');
}
return !isDisjointWith(inputType, this.convertInput(inputId, type));
return assignOk(this.convertInput(inputId, type), definitionType);
}

hasInput(inputId: InputId): boolean {
Expand Down Expand Up @@ -581,7 +581,7 @@ export class FunctionInstance {
const assignedType = partialInputs(id);
if (assignedType) {
const converted = definition.convertInput(id, assignedType);
const newType = intersect(converted, type);
const newType = assign(converted, type).assignedType;
if (newType.type === 'never') {
inputErrors.push({ inputId: id, inputType: type, assignedType });
}
Expand Down Expand Up @@ -666,10 +666,10 @@ export class FunctionInstance {
}

canAssign(inputId: InputId, type: Type): boolean {
const iType = this.definition.inputDefaults.get(inputId);
if (!iType) throw new Error(`Invalid input id ${inputId}`);
const definitionType = this.definition.inputDefaults.get(inputId);
if (!definitionType) throw new Error(`Invalid input id ${inputId}`);

// we say that types A is assignable to type B if they are not disjoint
return !isDisjointWith(iType, this.definition.convertInput(inputId, type));
return assignOk(this.definition.convertInput(inputId, type), definitionType);
}
}
7 changes: 6 additions & 1 deletion src/common/types/mismatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isStructInstance,
} from '@chainner/navi';
import { assertNever } from '../util';
import { assign } from './assign';
import { getChainnerScope } from './chainner-scope';
import { explain, formatChannelNumber } from './explain';
import { prettyPrintType } from './pretty';
Expand All @@ -31,11 +32,15 @@ export const generateAssignmentErrorTrace = (
assigned: Type,
definition: Type
): AssignmentErrorTrace | undefined => {
if (!isDisjointWith(assigned, definition)) {
const assignmentResult = assign(assigned, definition);
if (assignmentResult.isOk) {
// types compatible
return undefined;
}

// eslint-disable-next-line no-param-reassign
assigned = assignmentResult.errorType;

if (
isStructInstance(assigned) &&
isStructInstance(definition) &&
Expand Down

0 comments on commit 9f3e2b2

Please sign in to comment.