Skip to content
Open
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
46 changes: 44 additions & 2 deletions js/src/sails-idl-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { GearApi, HexString, UserMessageSent } from '@gear-js/api';
import { u8aToHex, u8aToU8a } from '@polkadot/util';

import type {
Type,
TypeDecl,
IIdlDoc,
IServiceExpo,
Expand Down Expand Up @@ -126,6 +127,10 @@ export class SailsProgram {
private _api?: GearApi;
private _programId?: HexString;
private _services: Map<bigint, IServiceUnit>;
// Lazy indices for resolveInService. Populated on first call; not invalidated because
// `_doc` is immutable after parse.
private _serviceTypeIndex?: Map<string, Map<string, Type>>;
private _programTypeIndex?: Map<string, Type>;
private _resolveServiceUnit = (ident: IServiceIdent): IServiceUnit | undefined => {
if (!ident.interface_id) {
throw new Error(`Service "${ident.name}" is missing interface_id in IDL`);
Expand All @@ -141,7 +146,7 @@ export class SailsProgram {
constructor(doc: IIdlDoc) {
this._doc = doc;
if (this._doc.program) {
this._typeResolver = new TypeResolver(this._doc.program.types);
this._typeResolver = new TypeResolver(this._doc.program.types ?? []);
}
this._services = this._initServices();
}
Expand Down Expand Up @@ -215,11 +220,43 @@ export class SailsProgram {
this._programId,
expo.route_idx,
this._resolveServiceUnit,
program.types ?? [],
);
}
return services;
}

/**
* Resolve a `TypeDecl` to its user-type definition in the scope of a named service.
* Service-local types shadow program-level types; program-level types are visible to
* every service. Returns `undefined` when the service doesn't exist, or the `TypeDecl`
* isn't a named user type.
*
* Backed by a lazy `Map`-based index — O(1) per call after the first invocation.
* Safe to call in tight loops while walking large IDL trees.
*/
resolveInService(serviceName: string, typeDecl: TypeDecl): Type | undefined {
if (typeof typeDecl === 'string' || typeDecl.kind !== 'named') return undefined;
if (!this._serviceTypeIndex) this._buildTypeIndex();
return (
this._serviceTypeIndex!.get(serviceName)?.get(typeDecl.name) ??
this._programTypeIndex!.get(typeDecl.name)
);
}
Comment thread
ukint-vs marked this conversation as resolved.

private _buildTypeIndex(): void {
const serviceIndex = new Map<string, Map<string, Type>>();
for (const unit of this._doc.services ?? []) {
const local = new Map<string, Type>();
for (const t of unit.types ?? []) local.set(t.name, t);
serviceIndex.set(unit.name, local);
}
const programIndex = new Map<string, Type>();
for (const t of this._doc.program?.types ?? []) programIndex.set(t.name, t);
this._serviceTypeIndex = serviceIndex;
this._programTypeIndex = programIndex;
}

/** #### Constructor functions with arguments from the parsed IDL */
get ctors(): Record<string, ISailsCtorFuncParams> | null {
if (!this._doc.program) {
Expand Down Expand Up @@ -332,6 +369,7 @@ export class SailsService implements ISailsService {
private _typeResolver: TypeResolver;
private _api?: GearApi;
private _routeIdx: number;
private _ambientTypes: Type[] = [];

private _resolveServiceUnit?: (ident: IServiceIdent) => IServiceUnit | undefined;

Expand All @@ -341,12 +379,15 @@ export class SailsService implements ISailsService {
programId?: HexString,
routeIdx = 0,
resolveServiceUnit?: (ident: IServiceIdent) => IServiceUnit | undefined,
ambientTypes: Type[] = [],
) {
this._service = service;
this._api = api;
this._programId = programId;
this._resolveServiceUnit = resolveServiceUnit;
this._typeResolver = new TypeResolver(service.types);
// Ambient types come first so service-local types shadow them on name collision.
this._typeResolver = new TypeResolver([...ambientTypes, ...(service.types ?? [])]);
this._ambientTypes = ambientTypes;
this._routeIdx = routeIdx;

this.events = this._getEvents(service);
Expand Down Expand Up @@ -569,6 +610,7 @@ export class SailsService implements ISailsService {
this._programId,
this.routeIdx,
this._resolveServiceUnit,
this._ambientTypes,
);
}

Expand Down
168 changes: 161 additions & 7 deletions js/src/type-resolver-idl-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,176 @@ export class TypeResolver {

const scaleTypes: Record<string, any> = {};
const userTypes: Record<string, Type> = {};
// Iteration order is last-write-wins: callers that want ambient-type shadowing should
// pass `[...ambientTypes, ...localTypes]` so locals overwrite on name collision.
for (const type of types) {
userTypes[type.name] = type;
}
this._userTypes = userTypes;
for (const type of Object.values(userTypes)) {
if (!type.type_params?.length) {
// register non-generic by name
scaleTypes[type.name] = this.getTypeDef(type);
}
}
this._userTypes = userTypes;
this.registry.setKnownTypes({ types: scaleTypes });
this.registry.register(scaleTypes);
}

/**
* Resolve a named user type to its `Type` definition.
*
* Two call shapes:
* - `resolveNamed(typeDecl)` — pass a `TypeDecl`; returns the user type for a
* `{ kind: 'named', name, generics? }` decl, or `undefined` for primitives, slices,
* arrays, tuples, type parameters, and unknown names.
* - `resolveNamed(name, generics?)` — pass the user type's name directly, with an
* optional concrete generics list.
*
* When concrete generics are provided (either via the overload or via `typeDecl.generics`),
* the returned `Type` is a **concrete** substituted copy: every `{ kind: 'generic', name }`
* leaf is replaced according to the user type's declared `type_params`, and `type_params`
* is omitted from the result. When no generics are provided, the raw user `Type` is
* returned as-is (shared with the resolver's internal state — do not mutate).
*/
resolveNamed(type: TypeDecl): Type | undefined;
resolveNamed(name: string, generics?: TypeDecl[]): Type | undefined;
resolveNamed(typeOrName: TypeDecl | string, generics?: TypeDecl[]): Type | undefined {
let name: string;
let concrete: TypeDecl[] | undefined;

if (typeof typeOrName === 'string') {
// String is ambiguous: it's either a primitive (`'u32'`) or a user-type name.
// Primitives aren't in `_userTypes`, so lookup naturally returns `undefined` for them.
name = typeOrName;
concrete = generics;
} else if (typeOrName.kind === 'named') {
name = typeOrName.name;
concrete = generics ?? typeOrName.generics;
} else {
return undefined;
}

const userType = this._userTypes[name];
if (!userType) return undefined;
if (!concrete?.length) return userType;

const substitutions = this._genericsSubstitutions(userType, concrete);
return this._resolveTypeGenerics(userType, substitutions);
}
Comment thread
vobradovich marked this conversation as resolved.

/**
* Recursively resolve type parameters through a `TypeDecl` tree.
*
* Pure: does not mutate inputs. Idempotent: passing an already-resolved tree yields an
* equivalent tree. Only `{ kind: 'generic', name: 'T' }` leaves whose `name` appears in
* `substitutions` are replaced; wrapper shapes (`Option<T>`, `Vec<T>`, `Result<T, E>`, custom
* generics, and any `named` decl) are preserved and their inner types resolved in place.
*
* Recurses through replacement chains (`{ T: U, U: u32 }` resolves `T` to `u32`).
* Cyclic maps (`{ T: { kind: 'generic', name: 'T' } }` or `{ T: U, U: T }`) are detected at
* runtime and throw rather than stack-overflowing.
*/
resolveGenerics(type: TypeDecl, substitutions: Record<string, TypeDecl> = {}): TypeDecl {
return this._resolveGenerics(type, substitutions, new Set());
}

private _resolveGenerics(
type: TypeDecl,
substitutions: Record<string, TypeDecl>,
visited: Set<string>,
): TypeDecl {
if (typeof type === 'string') return type;
if (type.kind === 'slice') {
const item = this._resolveGenerics(type.item, substitutions, visited);
return item === type.item ? type : { kind: 'slice', item };
}
if (type.kind === 'array') {
const item = this._resolveGenerics(type.item, substitutions, visited);
return item === type.item ? type : { kind: 'array', item, len: type.len };
}
if (type.kind === 'tuple') {
const next = type.types.map((t) => this._resolveGenerics(t, substitutions, visited));
return next.every((t, i) => t === type.types[i]) ? type : { kind: 'tuple', types: next };
}
if (type.kind === 'generic') {
// Explicit type-parameter leaf. Track visited names so a cyclic map
// (`{ T: T }`, `{ T: U, U: T }`) throws instead of stack-overflowing.
const replacement = substitutions[type.name];
if (replacement === undefined) return type;
if (visited.has(type.name)) {
throw new Error(
`Cyclic substitution detected while resolving type parameter "${type.name}" — ` +
`substitution chain: ${[...visited, type.name].join(' → ')}`,
);
}
const nextVisited = new Set(visited);
nextVisited.add(type.name);
return this._resolveGenerics(replacement, substitutions, nextVisited);
}
if (type.kind === 'named') {
if (!type.generics?.length) return type;
const next = type.generics.map((g) => this._resolveGenerics(g, substitutions, visited));
return next.every((g, i) => g === type.generics![i])
? type
: { kind: 'named', name: type.name, generics: next };
}
throw new Error('Unknown TypeDecl kind :: ' + JSON.stringify(type));
}

// Build a substitution map by zipping a user type's declared `type_params` with a concrete
// generics list. Internal: callers should use `resolveNamed(name, generics)` instead of
// building substitution maps by hand.
private _genericsSubstitutions(userType: Type, generics: TypeDecl[] = []): Record<string, TypeDecl> {
const map: Record<string, TypeDecl> = {};
const params = userType.type_params ?? [];
const len = Math.min(params.length, generics.length);
for (let i = 0; i < len; i++) {
map[params[i].name] = generics[i];
}
return map;
}

// Return a `Type` with every `generic` leaf substituted and `type_params` stripped. The result
// is a shallow copy — fields/variants/targets are recursively resolved via `resolveGenerics`,
// which preserves unchanged subtrees by reference.
private _resolveTypeGenerics(type: Type, substitutions: Record<string, TypeDecl>): Type {
if (type.kind === 'struct') {
return {
kind: 'struct',
name: type.name,
docs: type.docs,
annotations: type.annotations,
fields: type.fields.map((f) => ({
...f,
type: this.resolveGenerics(f.type, substitutions),
})),
};
}
if (type.kind === 'enum') {
return {
kind: 'enum',
name: type.name,
docs: type.docs,
annotations: type.annotations,
variants: type.variants.map((v) => ({
...v,
fields: v.fields.map((f) => ({
...f,
type: this.resolveGenerics(f.type, substitutions),
})),
})),
};
}
// alias
return {
kind: 'alias',
name: type.name,
docs: type.docs,
annotations: type.annotations,
target: this.resolveGenerics(type.target, substitutions),
};
}

/**
* Convert a `TypeDecl` into a concrete string name, resolving generic parameters.
*
Expand Down Expand Up @@ -131,11 +289,7 @@ export class TypeResolver {
.map((t: TypeDecl) => this.getTypeDeclString(t, generics, 'canonical'))
.join('')}`;
if (!this.registry.hasType(canonicalName)) {
// type param to generic map, i.e, { "T": "String", "U": { "kind": "named", "name": "Option", "generics": ["u32"]} }
const generics_map: Record<string, TypeDecl> = {};
for (let i = 0; i < userType.type_params.length; i++) {
generics_map[userType.type_params[i].name] = type.generics[i];
}
const generics_map = this._genericsSubstitutions(userType, type.generics);
const typeDef = this.getTypeDef(userType, generics_map);
/// When a user type with generics is resolved, the resolver constructs two names:
// - genericName: readable, type-like syntax (example MyType<Option<u32>>).
Expand Down
Loading
Loading