Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
103 changes: 96 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,111 @@ 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 `TypeDecl`'s named user type to its `Type` definition.
*
* Returns `undefined` for primitives, slices, arrays, tuples, type parameters
* (`{ kind: 'generic' }`), and unknown names. Does not recurse into generics — callers
* that want the substituted inner shape should pair this with {@link substituteGenerics}.
*
* The returned `Type` is shared with the resolver's internal state — do not mutate it.
*/
resolveNamed(type: TypeDecl): Type | undefined {
if (typeof type === 'string') return undefined;
if (type.kind !== 'named') return undefined;
return this._userTypes[type.name];
}
Comment thread
vobradovich marked this conversation as resolved.

/**
* Recursively substitute type parameters through a `TypeDecl` tree.
*
* Pure: does not mutate inputs. Idempotent: passing an already-substituted 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 substituted in place.
*
* The function 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 cause an error to be thrown rather than an unbounded recursion. Maps produced by
* {@link genericsSubstitutions} from a parsed IDL cannot create cycles.
*/
substituteGenerics(type: TypeDecl, substitutions: Record<string, TypeDecl> = {}): TypeDecl {
Comment thread
vobradovich marked this conversation as resolved.
Outdated
return this._substituteGenerics(type, substitutions, new Set());
}

private _substituteGenerics(
type: TypeDecl,
substitutions: Record<string, TypeDecl>,
visited: Set<string>,
): TypeDecl {
if (typeof type === 'string') return type;
if (type.kind === 'slice') {
const item = this._substituteGenerics(type.item, substitutions, visited);
return item === type.item ? type : { kind: 'slice', item };
}
if (type.kind === 'array') {
const item = this._substituteGenerics(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._substituteGenerics(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._substituteGenerics(replacement, substitutions, nextVisited);
}
if (type.kind === 'named') {
if (!type.generics?.length) return type;
const next = type.generics.map((g) => this._substituteGenerics(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 from a user type's declared `type_params` and a concrete
* generics list (typically the `generics` field of a `{ kind: 'named', generics: [...] }`
* `TypeDecl`). Missing positions are omitted.
*/
genericsSubstitutions(userType: Type, generics: TypeDecl[] = []): Record<string, TypeDecl> {
Comment thread
vobradovich marked this conversation as resolved.
Outdated
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;
}

/**
* Convert a `TypeDecl` into a concrete string name, resolving generic parameters.
*
Expand Down Expand Up @@ -131,11 +224,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