Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- fix: canonicalizes record and variant labels during subtype checking

## [3.0.1] - 2025-07-22

- fix: override `instanceof` in Candid IDL types to avoid issues when importing `IDL` from multiple locations.
Expand Down
6 changes: 6 additions & 0 deletions packages/candid/src/idl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,12 @@ describe('IDL subtyping', () => {
testSub(IDL.Tuple(IDL.Tuple(Odd)), Odd);
});

describe('Subtyping on records/variants normalizes field labels', () => {
// Checks we don't regress https://github.com/dfinity/agent-js/issues/1072
testSub(IDL.Record({ a: IDL.Nat, "_98_": IDL.Nat }), IDL.Record({ "_97_": IDL.Nat, b: IDL.Nat }));
testSub(IDL.Variant({ a: IDL.Nat, "_98_": IDL.Nat }), IDL.Variant({ "_97_": IDL.Nat, b: IDL.Nat }));
});

describe('decoding function/service references', () => {
const principal = Principal.fromText('w7x7r-cok77-xa');
it('checks subtyping when decoding function references', () => {
Expand Down
20 changes: 10 additions & 10 deletions packages/candid/src/idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1323,10 +1323,10 @@ export class RecordClass extends ConstructType<Record<string, any>> {
return x;
}

get fieldsAsObject(): Record<string, Type> {
const fields: Record<string, Type> = {};
get fieldsAsObject(): Record<number, Type> {
const fields: Record<number, Type> = {};
for (const [name, ty] of this._fields) {
fields[name] = ty;
fields[idlLabelToId(name)] = ty;
}
return fields;
}
Expand Down Expand Up @@ -1543,10 +1543,10 @@ export class VariantClass extends ConstructType<Record<string, any>> {
throw new Error('Variant has no data: ' + x);
}

get alternativesAsObject(): Record<string, Type> {
const alternatives: Record<string, Type> = {};
get alternativesAsObject(): Record<number, Type> {
const alternatives: Record<number, Type> = {};
for (const [name, ty] of this._fields) {
alternatives[name] = ty;
alternatives[idlLabelToId(name)] = ty;
}
return alternatives;
}
Expand Down Expand Up @@ -2438,8 +2438,8 @@ function subtype_(relations: Relations, t1: Type, t2: Type): boolean {
if (t2 instanceof OptClass) return true;
if (t1 instanceof RecordClass && t2 instanceof RecordClass) {
const t1Object = t1.fieldsAsObject;
for (const [name, ty2] of t2._fields) {
const ty1 = t1Object[name];
for (const [label, ty2] of t2._fields) {
const ty1 = t1Object[idlLabelToId(label)];
if (!ty1) {
if (!canBeOmmitted(ty2)) return false;
} else {
Expand Down Expand Up @@ -2472,8 +2472,8 @@ function subtype_(relations: Relations, t1: Type, t2: Type): boolean {

if (t1 instanceof VariantClass && t2 instanceof VariantClass) {
const t2Object = t2.alternativesAsObject;
for (const [name, ty1] of t1._fields) {
const ty2 = t2Object[name];
for (const [label, ty1] of t1._fields) {
const ty2 = t2Object[idlLabelToId(label)];
if (!ty2) return false;
if (!subtype_(relations, ty1, ty2)) return false;
}
Expand Down
Loading