Skip to content
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

Stricter condition for type assignability #2902

Merged
merged 1 commit into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading