UNPKG

@eliseev_s/tolk-tlb-transpiler

Version:

Transpile Tolk structs to TLB definitions and generate TypeScript wrappers for TON blockchain smart contracts

435 lines 18.3 kB
"use strict"; // ============================================================================ // TLB Generation Logic // ============================================================================ Object.defineProperty(exports, "__esModule", { value: true }); exports.TLBGenerator = void 0; const types_1 = require("./types"); const constants_1 = require("./constants"); const utils_1 = require("./utils"); const type_converters_1 = require("./type-converters"); const tlb_utils_1 = require("./tlb-utils"); /** * Utilities for common TLB generation patterns */ class TLBGenerationUtils { static formatFieldsToTLB(fields, typeConverter) { return fields.map((field) => `${field.name}:${typeConverter(field.type)}`).join(' '); } static formatAnonymousFields(items, typeConverter) { return items.map((item) => `_:${typeConverter(item)}`).join(' '); } static applyTypeMappingWithFallback(typeName) { return constants_1.TYPE_MAPPINGS[typeName] || typeName; } static validateUnionType(type) { return type.kind === 'union'; } static hasExactlyTwoAlternatives(unionType) { if (!TLBGenerationUtils.validateUnionType(unionType)) { return false; } return unionType.alternatives.length === 2; } static extractNonNullTypeFromUnion(unionType) { if (!TLBGenerationUtils.validateUnionType(unionType)) return null; return (unionType.alternatives.find((alt) => !(alt.kind === 'type_identifier' && alt.name === 'null')) || null); } static createUnionTypeName(unionType) { if (!TLBGenerationUtils.validateUnionType(unionType)) return 'Cell'; const variantNames = unionType.alternatives .map((alt) => { if (alt.kind === 'type_identifier') return alt.name; if (alt.kind === 'primitive') return alt.name; return 'Variant'; }) .join(''); return `${variantNames}Union`; } } /** * Handlers for different union strategies */ class UnionStrategyHandlers { context; typeConverter; constructor(context, typeConverter) { this.context = context; this.typeConverter = typeConverter; } handleMaybePattern(unionType) { const nonNullType = TLBGenerationUtils.extractNonNullTypeFromUnion(unionType); if (nonNullType) { return (0, tlb_utils_1.formatMaybeType)(this.typeConverter(nonNullType)); } return 'Cell'; } handleEitherPattern(unionType) { if (!TLBGenerationUtils.hasExactlyTwoAlternatives(unionType)) { return 'Cell'; } const [leftType, rightType] = unionType.alternatives; if (leftType && rightType) { const leftTLB = this.typeConverter(leftType); const rightTLB = this.typeConverter(rightType); return (0, tlb_utils_1.formatEitherType)(leftTLB, rightTLB); } return 'Cell'; } handleCustomPrefix(unionType) { const unionTypeName = TLBGenerationUtils.createUnionTypeName(unionType); this.context.inlineUnions.set(unionTypeName, unionType); return unionTypeName; } } /** * Specialized type converters for different TolkType kinds */ class TypeConverters { context; mainConverter; constructor(context, mainConverter) { this.context = context; this.mainConverter = mainConverter; } convertPrimitive(type) { return TLBGenerationUtils.applyTypeMappingWithFallback(type.name); } convertTypeIdentifier(type) { // Handle generic type written as identifier with angle brackets, e.g., Foo<Bar,Baz> const genericMatch = type.name.match(/^(\w+)<(.+)>$/); if (genericMatch && genericMatch[1] && genericMatch[2]) { const genericName = genericMatch[1]; const paramsText = genericMatch[2]; // Special-case Cell<...> if (genericName === 'Cell') { const params = this.splitGenericParams(paramsText).map((p) => p.trim()); const inner = params[0] || ''; return (0, tlb_utils_1.formatCellRefType)(this.mainConverter({ kind: 'type_identifier', name: inner })); } const params = this.splitGenericParams(paramsText) .map((p) => p.trim()) .filter((p) => p.length > 0) .map((p) => this.mainConverter({ kind: 'type_identifier', name: p })); const renderedParams = params.join(' '); return `(${genericName} ${renderedParams})`; } const typeAlias = this.context.typeAliases.get(type.name); if (typeAlias?.underlyingType.kind === 'union') { const strategy = UnionStrategyDeterminer.determine(typeAlias.underlyingType, this.context); if (strategy === types_1.UnionStrategy.MAYBE_PATTERN) { const handlers = new UnionStrategyHandlers(this.context, this.mainConverter); return handlers.handleMaybePattern(typeAlias.underlyingType); } } return TLBGenerationUtils.applyTypeMappingWithFallback(type.name); } splitGenericParams(paramsText) { const parts = []; let depth = 0; let current = ''; for (let i = 0; i < paramsText.length; i++) { const ch = paramsText[i]; if (ch === '<') { depth++; current += ch; } else if (ch === '>') { depth--; current += ch; } else if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else { current += ch; } } if (current.trim().length > 0) parts.push(current); return parts; } convertGeneric(type) { // Handle Cell<T> specially if (type.name === 'Cell' && type.params.length > 0 && type.params[0]) { return (0, tlb_utils_1.formatCellRefType)(this.mainConverter(type.params[0])); } // Handle map<K,V> specially -> HashmapE n V if (type.name.toLowerCase() === 'map' && type.params.length >= 2) { const keyBits = (0, type_converters_1.getMapKeyBitWidth)(type.params[0]); if (!keyBits || isNaN(keyBits)) { return '(Maybe ^Cell)'; } const valueTLB = this.mainConverter(type.params[1]); const valueNeedsParens = valueTLB.includes(' ') && !(valueTLB.startsWith('(') && valueTLB.endsWith(')')); const renderedValue = valueNeedsParens ? `(${valueTLB})` : valueTLB; return `(HashmapE ${keyBits} ${renderedValue})`; } // Handle generic type aliases const genericTypeAlias = this.context.typeAliases.get(type.name); if (genericTypeAlias?.underlyingType.kind === 'union') { return this.handleGenericUnion(type, genericTypeAlias); } // Render generic type application as a single parenthesized expression when it has params const renderedParams = type.params.map((p) => this.mainConverter(p)).join(' '); return renderedParams ? `(${type.name} ${renderedParams})` : type.name; } handleGenericUnion(type, typeAlias) { const strategy = UnionStrategyDeterminer.determine(typeAlias.underlyingType, this.context); if (strategy === types_1.UnionStrategy.EITHER_PATTERN && type.params.length === 2) { const [leftType, rightType] = type.params; if (leftType && rightType) { const leftTLB = this.mainConverter(leftType); const rightTLB = this.mainConverter(rightType); return (0, tlb_utils_1.formatEitherType)(leftTLB, rightTLB); } } if (strategy === types_1.UnionStrategy.MAYBE_PATTERN && type.params.length === 1 && type.params[0]) { return (0, tlb_utils_1.formatMaybeType)(this.mainConverter(type.params[0])); } return type.name; } convertCollectionType(type) { const items = TLBGenerationUtils.formatAnonymousFields(type.items, this.mainConverter); return (0, tlb_utils_1.formatCollectionType)(items); } convertSizedType(type) { switch (type.kind) { case 'bits': return `bits${type.width}`; case 'bytes': return `bits${(type.size || 0) * 8}`; case 'int': return `int${type.width}`; case 'uint': return `uint${type.width}`; default: return 'Cell'; } } convertSpecialType(type) { return TLBGenerationUtils.applyTypeMappingWithFallback(type.name) || 'Cell'; } convertCellRef(type) { return type.inner ? (0, tlb_utils_1.formatCellRefType)(this.mainConverter(type.inner)) : '^Cell'; } convertNullable(type) { // In Tolk 1.2+, optional address (address?) uses two-zero-bits encoding // which is already part of MsgAddress, not the TLB Maybe pattern const inner = type.inner; if ((inner.kind === 'primitive' && inner.name === 'address') || (inner.kind === 'type_identifier' && inner.name === 'address')) { return 'MsgAddress'; } return (0, tlb_utils_1.formatMaybeType)(this.mainConverter(type.inner)); } } /** * Union strategy determination logic */ class UnionStrategyDeterminer { static determine(unionType, context) { if (!TLBGenerationUtils.validateUnionType(unionType)) { return types_1.UnionStrategy.CUSTOM_PREFIX; } // Check for Maybe pattern (T | null) if (TLBGenerationUtils.hasExactlyTwoAlternatives(unionType)) { const hasNull = unionType.alternatives.some((alt) => alt.kind === 'type_identifier' && alt.name === 'null'); if (hasNull) return types_1.UnionStrategy.MAYBE_PATTERN; } // Check for Either pattern (2 alternatives, no explicit prefixes) if (TLBGenerationUtils.hasExactlyTwoAlternatives(unionType)) { const hasExplicitPrefixes = unionType.alternatives.every((alt) => { if (alt.kind === 'type_identifier') { const struct = context.structs.get(alt.name); return struct?.prefix; } return false; }); if (!hasExplicitPrefixes) return types_1.UnionStrategy.EITHER_PATTERN; } return types_1.UnionStrategy.CUSTOM_PREFIX; } } class TLBGenerator { context; constructor(structs, typeAliases) { this.context = { structs, typeAliases, tagRegistry: new Map(), nextAutoTag: 0, inlineUnions: new Map(), builtInTypes: new Set([ 'Maybe', 'Either', 'Bool', 'Cell', 'Tuple', 'Grams', 'MsgAddress', ]), }; } generateTLB() { const tlbDefinitions = []; // Clear previous state for fresh generation this.context.inlineUnions.clear(); // Process struct declarations for (const [name, struct] of this.context.structs) { const tlb = this.generateStructTLB(struct); tlbDefinitions.push(tlb); } // Process type alias declarations for (const [name, typeAlias] of this.context.typeAliases) { if (typeAlias.underlyingType.kind === 'union') { const tlb = this.generateUnionTLB(name, typeAlias.underlyingType); tlbDefinitions.push(tlb); } } // Process inline unions discovered during processing for (const [unionTypeName, unionType] of this.context.inlineUnions) { const tlb = this.generateUnionTLB(unionTypeName, unionType); tlbDefinitions.push(tlb); } return tlbDefinitions.filter((def) => def.statement.length > 0); } generateStructTLB(struct) { const constructorName = (0, utils_1.toSnakeCase)(struct.name); const prefix = struct.prefix ? (0, tlb_utils_1.formatPrefix)(struct.prefix) : '#_'; const fields = TLBGenerationUtils.formatFieldsToTLB(struct.fields, (type) => this.tolkTypeToTLB(type)); const genericDecl = struct.genericParams && struct.genericParams.length > 0 ? ' ' + struct.genericParams.map((p) => `{${p.name}:Type}`).join(' ') : ''; const typeArgs = struct.genericParams && struct.genericParams.length > 0 ? ' ' + struct.genericParams.map((p) => p.name).join(' ') : ''; const constructor = `${constructorName}${prefix}${genericDecl} ${fields} = ${struct.name}${typeArgs};`; return { name: struct.name, statement: [constructor], }; } generateUnionTLB(typeName, unionType) { if (!TLBGenerationUtils.validateUnionType(unionType)) { return { name: typeName, statement: [] }; } const strategy = UnionStrategyDeterminer.determine(unionType, this.context); switch (strategy) { case types_1.UnionStrategy.MAYBE_PATTERN: case types_1.UnionStrategy.EITHER_PATTERN: // Built-in types - don't generate definitions return { name: typeName, statement: [] }; case types_1.UnionStrategy.CUSTOM_PREFIX: return this.generateCustomPrefixTLB(typeName, unionType); default: return { name: typeName, statement: [] }; } } generateCustomPrefixTLB(typeName, unionType) { if (!TLBGenerationUtils.validateUnionType(unionType)) { return { name: typeName, statement: [] }; } const constructors = []; for (const alt of unionType.alternatives) { if (alt.kind === 'type_identifier') { const struct = this.context.structs.get(alt.name); if (struct) { const constructor = this.generateUnionConstructor(alt.name, struct, typeName, unionType.alternatives.length); constructors.push(constructor); } } } return { name: typeName, statement: constructors }; } generateUnionConstructor(altName, struct, typeName, totalAlternatives) { const tag = this.getOrAssignTag(altName, struct.prefix); const fields = TLBGenerationUtils.formatFieldsToTLB(struct.fields, (type) => this.tolkTypeToTLB(type)); const constructorName = (0, utils_1.toSnakeCase)(altName); const prefix = (0, tlb_utils_1.formatUnionPrefix)(tag, struct.prefix, totalAlternatives); return (0, tlb_utils_1.buildConstructor)(constructorName, prefix, fields, typeName); } getOrAssignTag(typeName, explicitPrefix) { if (explicitPrefix !== undefined) { this.context.tagRegistry.set(typeName, explicitPrefix.value); return explicitPrefix.value; } if (this.context.tagRegistry.has(typeName)) { return this.context.tagRegistry.get(typeName); } const autoTag = this.context.nextAutoTag++; this.context.tagRegistry.set(typeName, autoTag); return autoTag; } tolkTypeToTLB(type) { const converters = new TypeConverters(this.context, (t) => this.tolkTypeToTLB(t)); const handlers = new UnionStrategyHandlers(this.context, (t) => this.tolkTypeToTLB(t)); switch (type.kind) { case 'primitive': return converters.convertPrimitive(type); case 'type_identifier': return converters.convertTypeIdentifier(type); case 'nullable': return converters.convertNullable(type); case 'union': return this.generateInlineUnionType(type, handlers); case 'generic': return converters.convertGeneric(type); case 'tensor': case 'tuple': return converters.convertCollectionType(type); case 'bits': case 'bytes': case 'int': case 'uint': return converters.convertSizedType(type); case 'cell_ref': return converters.convertCellRef(type); case 'map': { const keyBits = (0, type_converters_1.getMapKeyBitWidth)(type.key); if (!keyBits || isNaN(keyBits)) { // Default: treat as cell if unsupported key type return '(Maybe ^Cell)'; } const valueTLB = this.tolkTypeToTLB(type.value); const valueNeedsParens = valueTLB.includes(' ') && !(valueTLB.startsWith('(') && valueTLB.endsWith(')')); const renderedValue = valueNeedsParens ? `(${valueTLB})` : valueTLB; return `(HashmapE ${keyBits} ${renderedValue})`; } case 'dict': return '(Maybe ^Cell)'; case 'special': return converters.convertSpecialType(type); default: return 'Cell'; } } generateInlineUnionType(unionType, handlers) { if (!TLBGenerationUtils.validateUnionType(unionType)) return 'Cell'; const strategy = UnionStrategyDeterminer.determine(unionType, this.context); switch (strategy) { case types_1.UnionStrategy.MAYBE_PATTERN: return handlers.handleMaybePattern(unionType); case types_1.UnionStrategy.EITHER_PATTERN: return handlers.handleEitherPattern(unionType); case types_1.UnionStrategy.CUSTOM_PREFIX: return handlers.handleCustomPrefix(unionType); default: return 'Cell'; } } } exports.TLBGenerator = TLBGenerator; //# sourceMappingURL=generator.js.map