@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
JavaScript
"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