mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Struct types.
This commit is contained in:
parent
46dd629820
commit
c560a62352
13
AST.cpp
13
AST.cpp
@ -460,8 +460,17 @@ bool FunctionCall::isTypeConversion() const
|
|||||||
|
|
||||||
void MemberAccess::checkTypeRequirements()
|
void MemberAccess::checkTypeRequirements()
|
||||||
{
|
{
|
||||||
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Member access not yet implemented."));
|
m_expression->checkTypeRequirements();
|
||||||
// m_type = ;
|
m_expression->requireLValue();
|
||||||
|
if (m_expression->getType()->getCategory() != Type::Category::STRUCT)
|
||||||
|
BOOST_THROW_EXCEPTION(createTypeError("Member access to a non-struct (is " +
|
||||||
|
m_expression->getType()->toString() + ")"));
|
||||||
|
StructType const& type = dynamic_cast<StructType const&>(*m_expression->getType());
|
||||||
|
unsigned memberIndex = type.memberNameToIndex(*m_memberName);
|
||||||
|
if (memberIndex >= type.getMemberCount())
|
||||||
|
BOOST_THROW_EXCEPTION(createTypeError("Member \"" + *m_memberName + "\" not found in " + type.toString()));
|
||||||
|
m_type = type.getMemberByIndex(memberIndex).getType();
|
||||||
|
m_isLvalue = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IndexAccess::checkTypeRequirements()
|
void IndexAccess::checkTypeRequirements()
|
||||||
|
5
AST.h
5
AST.h
@ -146,7 +146,7 @@ private:
|
|||||||
/**
|
/**
|
||||||
* Parameter list, used as function parameter list and return list.
|
* Parameter list, used as function parameter list and return list.
|
||||||
* None of the parameters is allowed to contain mappings (not even recursively
|
* None of the parameters is allowed to contain mappings (not even recursively
|
||||||
* inside structs), but (@todo) this is not yet enforced.
|
* inside structs).
|
||||||
*/
|
*/
|
||||||
class ParameterList: public ASTNode
|
class ParameterList: public ASTNode
|
||||||
{
|
{
|
||||||
@ -368,7 +368,6 @@ private:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Statement in which a break statement is legal.
|
* Statement in which a break statement is legal.
|
||||||
* @todo actually check this requirement.
|
|
||||||
*/
|
*/
|
||||||
class BreakableStatement: public Statement
|
class BreakableStatement: public Statement
|
||||||
{
|
{
|
||||||
@ -629,6 +628,7 @@ public:
|
|||||||
ASTPointer<ASTString> const& _memberName):
|
ASTPointer<ASTString> const& _memberName):
|
||||||
Expression(_location), m_expression(_expression), m_memberName(_memberName) {}
|
Expression(_location), m_expression(_expression), m_memberName(_memberName) {}
|
||||||
virtual void accept(ASTVisitor& _visitor) override;
|
virtual void accept(ASTVisitor& _visitor) override;
|
||||||
|
Expression& getExpression() const { return *m_expression; }
|
||||||
ASTString const& getMemberName() const { return *m_memberName; }
|
ASTString const& getMemberName() const { return *m_memberName; }
|
||||||
virtual void checkTypeRequirements() override;
|
virtual void checkTypeRequirements() override;
|
||||||
|
|
||||||
@ -651,6 +651,7 @@ public:
|
|||||||
|
|
||||||
Expression& getBaseExpression() const { return *m_base; }
|
Expression& getBaseExpression() const { return *m_base; }
|
||||||
Expression& getIndexExpression() const { return *m_index; }
|
Expression& getIndexExpression() const { return *m_index; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ASTPointer<Expression> m_base;
|
ASTPointer<Expression> m_base;
|
||||||
ASTPointer<Expression> m_index;
|
ASTPointer<Expression> m_index;
|
||||||
|
@ -49,7 +49,6 @@ bool ExpressionCompiler::visit(Assignment& _assignment)
|
|||||||
{
|
{
|
||||||
_assignment.getRightHandSide().accept(*this);
|
_assignment.getRightHandSide().accept(*this);
|
||||||
appendTypeConversion(*_assignment.getRightHandSide().getType(), *_assignment.getType());
|
appendTypeConversion(*_assignment.getRightHandSide().getType(), *_assignment.getType());
|
||||||
m_currentLValue.reset();
|
|
||||||
_assignment.getLeftHandSide().accept(*this);
|
_assignment.getLeftHandSide().accept(*this);
|
||||||
if (asserts(m_currentLValue.isValid()))
|
if (asserts(m_currentLValue.isValid()))
|
||||||
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("LValue not retrieved."));
|
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("LValue not retrieved."));
|
||||||
@ -63,6 +62,7 @@ bool ExpressionCompiler::visit(Assignment& _assignment)
|
|||||||
appendOrdinaryBinaryOperatorCode(Token::AssignmentToBinaryOp(op), *_assignment.getType());
|
appendOrdinaryBinaryOperatorCode(Token::AssignmentToBinaryOp(op), *_assignment.getType());
|
||||||
}
|
}
|
||||||
m_currentLValue.storeValue(_assignment);
|
m_currentLValue.storeValue(_assignment);
|
||||||
|
m_currentLValue.reset();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -90,6 +90,7 @@ void ExpressionCompiler::endVisit(UnaryOperation& _unaryOperation)
|
|||||||
if (m_currentLValue.storesReferenceOnStack())
|
if (m_currentLValue.storesReferenceOnStack())
|
||||||
m_context << eth::Instruction::SWAP1;
|
m_context << eth::Instruction::SWAP1;
|
||||||
m_currentLValue.storeValue(_unaryOperation);
|
m_currentLValue.storeValue(_unaryOperation);
|
||||||
|
m_currentLValue.reset();
|
||||||
break;
|
break;
|
||||||
case Token::INC: // ++ (pre- or postfix)
|
case Token::INC: // ++ (pre- or postfix)
|
||||||
case Token::DEC: // -- (pre- or postfix)
|
case Token::DEC: // -- (pre- or postfix)
|
||||||
@ -113,6 +114,7 @@ void ExpressionCompiler::endVisit(UnaryOperation& _unaryOperation)
|
|||||||
if (m_currentLValue.storesReferenceOnStack())
|
if (m_currentLValue.storesReferenceOnStack())
|
||||||
m_context << eth::Instruction::SWAP1;
|
m_context << eth::Instruction::SWAP1;
|
||||||
m_currentLValue.storeValue(_unaryOperation, !_unaryOperation.isPrefixOperation());
|
m_currentLValue.storeValue(_unaryOperation, !_unaryOperation.isPrefixOperation());
|
||||||
|
m_currentLValue.reset();
|
||||||
break;
|
break;
|
||||||
case Token::ADD: // +
|
case Token::ADD: // +
|
||||||
// unary add, so basically no-op
|
// unary add, so basically no-op
|
||||||
@ -182,10 +184,10 @@ bool ExpressionCompiler::visit(FunctionCall& _functionCall)
|
|||||||
arguments[i]->accept(*this);
|
arguments[i]->accept(*this);
|
||||||
appendTypeConversion(*arguments[i]->getType(), *function.getParameters()[i]->getType());
|
appendTypeConversion(*arguments[i]->getType(), *function.getParameters()[i]->getType());
|
||||||
}
|
}
|
||||||
m_currentLValue.reset();
|
|
||||||
_functionCall.getExpression().accept(*this);
|
_functionCall.getExpression().accept(*this);
|
||||||
if (asserts(m_currentLValue.isInCode()))
|
if (asserts(m_currentLValue.isInCode()))
|
||||||
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Code reference expected."));
|
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Code reference expected."));
|
||||||
|
m_currentLValue.reset();
|
||||||
|
|
||||||
m_context.appendJump();
|
m_context.appendJump();
|
||||||
m_context << returnLabel;
|
m_context << returnLabel;
|
||||||
@ -201,9 +203,16 @@ bool ExpressionCompiler::visit(FunctionCall& _functionCall)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExpressionCompiler::endVisit(MemberAccess&)
|
void ExpressionCompiler::endVisit(MemberAccess& _memberAccess)
|
||||||
{
|
{
|
||||||
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Member access not yet implemented."));
|
if (asserts(m_currentLValue.isInStorage()))
|
||||||
|
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Member access to a non-storage value."));
|
||||||
|
StructType const& type = dynamic_cast<StructType const&>(*_memberAccess.getExpression().getType());
|
||||||
|
unsigned memberIndex = type.memberNameToIndex(_memberAccess.getMemberName());
|
||||||
|
if (asserts(memberIndex <= type.getMemberCount()))
|
||||||
|
BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Member not found in struct during compilation."));
|
||||||
|
m_context << type.getStorageOffsetOfMember(memberIndex) << eth::Instruction::ADD;
|
||||||
|
m_currentLValue.retrieveValueIfLValueNotRequested(_memberAccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpressionCompiler::visit(IndexAccess& _indexAccess)
|
bool ExpressionCompiler::visit(IndexAccess& _indexAccess)
|
||||||
|
@ -37,7 +37,10 @@ void NameAndTypeResolver::resolveNamesAndTypes(ContractDefinition& _contract)
|
|||||||
reset();
|
reset();
|
||||||
DeclarationRegistrationHelper registrar(m_scopes, _contract);
|
DeclarationRegistrationHelper registrar(m_scopes, _contract);
|
||||||
m_currentScope = &m_scopes[&_contract];
|
m_currentScope = &m_scopes[&_contract];
|
||||||
//@todo structs
|
for (ASTPointer<StructDefinition> const& structDef: _contract.getDefinedStructs())
|
||||||
|
ReferencesResolver resolver(*structDef, *this, nullptr);
|
||||||
|
for (ASTPointer<StructDefinition> const& structDef: _contract.getDefinedStructs())
|
||||||
|
checkForRecursion(*structDef);
|
||||||
for (ASTPointer<VariableDeclaration> const& variable: _contract.getStateVariables())
|
for (ASTPointer<VariableDeclaration> const& variable: _contract.getStateVariables())
|
||||||
ReferencesResolver resolver(*variable, *this, nullptr);
|
ReferencesResolver resolver(*variable, *this, nullptr);
|
||||||
for (ASTPointer<FunctionDefinition> const& function: _contract.getDefinedFunctions())
|
for (ASTPointer<FunctionDefinition> const& function: _contract.getDefinedFunctions())
|
||||||
@ -70,6 +73,24 @@ Declaration* NameAndTypeResolver::getNameFromCurrentScope(ASTString const& _name
|
|||||||
return m_currentScope->resolveName(_name, _recursive);
|
return m_currentScope->resolveName(_name, _recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NameAndTypeResolver::checkForRecursion(StructDefinition const& _struct)
|
||||||
|
{
|
||||||
|
set<StructDefinition const*> definitionsSeen;
|
||||||
|
vector<StructDefinition const*> queue = {&_struct};
|
||||||
|
while (!queue.empty())
|
||||||
|
{
|
||||||
|
StructDefinition const* def = queue.back();
|
||||||
|
queue.pop_back();
|
||||||
|
if (definitionsSeen.count(def))
|
||||||
|
BOOST_THROW_EXCEPTION(ParserError() << errinfo_sourceLocation(def->getLocation())
|
||||||
|
<< errinfo_comment("Recursive struct definition."));
|
||||||
|
definitionsSeen.insert(def);
|
||||||
|
for (ASTPointer<VariableDeclaration> const& member: def->getMembers())
|
||||||
|
if (member->getType()->getCategory() == Type::Category::STRUCT)
|
||||||
|
queue.push_back(dynamic_cast<UserDefinedTypeName&>(*member->getTypeName()).getReferencedStruct());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void NameAndTypeResolver::reset()
|
void NameAndTypeResolver::reset()
|
||||||
{
|
{
|
||||||
m_scopes.clear();
|
m_scopes.clear();
|
||||||
@ -163,8 +184,8 @@ void DeclarationRegistrationHelper::registerDeclaration(Declaration& _declaratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
ReferencesResolver::ReferencesResolver(ASTNode& _root, NameAndTypeResolver& _resolver,
|
ReferencesResolver::ReferencesResolver(ASTNode& _root, NameAndTypeResolver& _resolver,
|
||||||
ParameterList* _returnParameters):
|
ParameterList* _returnParameters, bool _allowLazyTypes):
|
||||||
m_resolver(_resolver), m_returnParameters(_returnParameters)
|
m_resolver(_resolver), m_returnParameters(_returnParameters), m_allowLazyTypes(_allowLazyTypes)
|
||||||
{
|
{
|
||||||
_root.accept(*this);
|
_root.accept(*this);
|
||||||
}
|
}
|
||||||
@ -175,6 +196,8 @@ void ReferencesResolver::endVisit(VariableDeclaration& _variable)
|
|||||||
// or mapping
|
// or mapping
|
||||||
if (_variable.getTypeName())
|
if (_variable.getTypeName())
|
||||||
_variable.setType(_variable.getTypeName()->toType());
|
_variable.setType(_variable.getTypeName()->toType());
|
||||||
|
else if (!m_allowLazyTypes)
|
||||||
|
BOOST_THROW_EXCEPTION(_variable.createTypeError("Explicit type needed."));
|
||||||
// otherwise we have a "var"-declaration whose type is resolved by the first assignment
|
// otherwise we have a "var"-declaration whose type is resolved by the first assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,10 +55,13 @@ public:
|
|||||||
Declaration* getNameFromCurrentScope(ASTString const& _name, bool _recursive = true);
|
Declaration* getNameFromCurrentScope(ASTString const& _name, bool _recursive = true);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/// Throws if @a _struct contains a recursive loop. Note that recursion via mappings is fine.
|
||||||
|
void checkForRecursion(StructDefinition const& _struct);
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
/// Maps nodes declaring a scope to scopes, i.e. ContractDefinition, FunctionDeclaration and
|
/// Maps nodes declaring a scope to scopes, i.e. ContractDefinition and FunctionDeclaration,
|
||||||
/// StructDefinition (@todo not yet implemented), where nullptr denotes the global scope.
|
/// where nullptr denotes the global scope. Note that structs are not scope since they do
|
||||||
|
/// not contain code.
|
||||||
std::map<ASTNode const*, Scope> m_scopes;
|
std::map<ASTNode const*, Scope> m_scopes;
|
||||||
|
|
||||||
Scope* m_currentScope;
|
Scope* m_currentScope;
|
||||||
@ -99,7 +102,8 @@ private:
|
|||||||
class ReferencesResolver: private ASTVisitor
|
class ReferencesResolver: private ASTVisitor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ReferencesResolver(ASTNode& _root, NameAndTypeResolver& _resolver, ParameterList* _returnParameters);
|
ReferencesResolver(ASTNode& _root, NameAndTypeResolver& _resolver,
|
||||||
|
ParameterList* _returnParameters, bool _allowLazyTypes = true);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual void endVisit(VariableDeclaration& _variable) override;
|
virtual void endVisit(VariableDeclaration& _variable) override;
|
||||||
@ -110,6 +114,7 @@ private:
|
|||||||
|
|
||||||
NameAndTypeResolver& m_resolver;
|
NameAndTypeResolver& m_resolver;
|
||||||
ParameterList* m_returnParameters;
|
ParameterList* m_returnParameters;
|
||||||
|
bool m_allowLazyTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
55
Types.cpp
55
Types.cpp
@ -80,7 +80,7 @@ shared_ptr<Type> Type::forLiteral(Literal const& _literal)
|
|||||||
case Token::NUMBER:
|
case Token::NUMBER:
|
||||||
return IntegerType::smallestTypeForLiteral(_literal.getValue());
|
return IntegerType::smallestTypeForLiteral(_literal.getValue());
|
||||||
case Token::STRING_LITERAL:
|
case Token::STRING_LITERAL:
|
||||||
return shared_ptr<Type>(); // @todo
|
return shared_ptr<Type>(); // @todo add string literals
|
||||||
default:
|
default:
|
||||||
return shared_ptr<Type>();
|
return shared_ptr<Type>();
|
||||||
}
|
}
|
||||||
@ -231,6 +231,48 @@ u256 StructType::getStorageSize() const
|
|||||||
return max<u256>(1, size);
|
return max<u256>(1, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StructType::canLiveOutsideStorage() const
|
||||||
|
{
|
||||||
|
for (unsigned i = 0; i < getMemberCount(); ++i)
|
||||||
|
if (!getMemberByIndex(i).getType()->canLiveOutsideStorage())
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
string StructType::toString() const
|
||||||
|
{
|
||||||
|
return string("struct ") + m_struct.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned StructType::getMemberCount() const
|
||||||
|
{
|
||||||
|
return m_struct.getMembers().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned StructType::memberNameToIndex(string const& _name) const
|
||||||
|
{
|
||||||
|
vector<ASTPointer<VariableDeclaration>> const& members = m_struct.getMembers();
|
||||||
|
for (unsigned index = 0; index < members.size(); ++index)
|
||||||
|
if (members[index]->getName() == _name)
|
||||||
|
return index;
|
||||||
|
return unsigned(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
VariableDeclaration const& StructType::getMemberByIndex(unsigned _index) const
|
||||||
|
{
|
||||||
|
return *m_struct.getMembers()[_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
u256 StructType::getStorageOffsetOfMember(unsigned _index) const
|
||||||
|
{
|
||||||
|
//@todo cache member offset?
|
||||||
|
u256 offset;
|
||||||
|
vector<ASTPointer<VariableDeclaration>> const& members = m_struct.getMembers();
|
||||||
|
for (unsigned index = 0; index < _index; ++index)
|
||||||
|
offset += getMemberByIndex(index).getType()->getStorageSize();
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
bool FunctionType::operator==(Type const& _other) const
|
bool FunctionType::operator==(Type const& _other) const
|
||||||
{
|
{
|
||||||
if (_other.getCategory() != getCategory())
|
if (_other.getCategory() != getCategory())
|
||||||
@ -239,6 +281,12 @@ bool FunctionType::operator==(Type const& _other) const
|
|||||||
return other.m_function == m_function;
|
return other.m_function == m_function;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string FunctionType::toString() const
|
||||||
|
{
|
||||||
|
//@todo nice string for function types
|
||||||
|
return "function(...)returns(...)";
|
||||||
|
}
|
||||||
|
|
||||||
bool MappingType::operator==(Type const& _other) const
|
bool MappingType::operator==(Type const& _other) const
|
||||||
{
|
{
|
||||||
if (_other.getCategory() != getCategory())
|
if (_other.getCategory() != getCategory())
|
||||||
@ -247,6 +295,11 @@ bool MappingType::operator==(Type const& _other) const
|
|||||||
return *other.m_keyType == *m_keyType && *other.m_valueType == *m_valueType;
|
return *other.m_keyType == *m_keyType && *other.m_valueType == *m_valueType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string MappingType::toString() const
|
||||||
|
{
|
||||||
|
return "mapping(" + getKeyType()->toString() + " => " + getValueType()->toString() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
bool TypeType::operator==(Type const& _other) const
|
bool TypeType::operator==(Type const& _other) const
|
||||||
{
|
{
|
||||||
if (_other.getCategory() != getCategory())
|
if (_other.getCategory() != getCategory())
|
||||||
|
16
Types.h
16
Types.h
@ -184,9 +184,14 @@ public:
|
|||||||
|
|
||||||
virtual bool operator==(Type const& _other) const override;
|
virtual bool operator==(Type const& _other) const override;
|
||||||
virtual u256 getStorageSize() const;
|
virtual u256 getStorageSize() const;
|
||||||
//@todo it can, if its members can
|
virtual bool canLiveOutsideStorage() const;
|
||||||
virtual bool canLiveOutsideStorage() const { return false; }
|
virtual std::string toString() const override;
|
||||||
virtual std::string toString() const override { return "struct{...}"; }
|
|
||||||
|
unsigned getMemberCount() const;
|
||||||
|
/// Returns the index of the member with name @a _name or unsigned(-1) if it does not exist.
|
||||||
|
unsigned memberNameToIndex(std::string const& _name) const;
|
||||||
|
VariableDeclaration const& getMemberByIndex(unsigned _index) const;
|
||||||
|
u256 getStorageOffsetOfMember(unsigned _index) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
StructDefinition const& m_struct;
|
StructDefinition const& m_struct;
|
||||||
@ -204,7 +209,7 @@ public:
|
|||||||
FunctionDefinition const& getFunction() const { return m_function; }
|
FunctionDefinition const& getFunction() const { return m_function; }
|
||||||
|
|
||||||
virtual bool operator==(Type const& _other) const override;
|
virtual bool operator==(Type const& _other) const override;
|
||||||
virtual std::string toString() const override { return "function(...)returns(...)"; }
|
virtual std::string toString() const override;
|
||||||
virtual u256 getStorageSize() const { BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Storage size of non-storable function type requested.")); }
|
virtual u256 getStorageSize() const { BOOST_THROW_EXCEPTION(InternalCompilerError() << errinfo_comment("Storage size of non-storable function type requested.")); }
|
||||||
virtual bool canLiveOutsideStorage() const { return false; }
|
virtual bool canLiveOutsideStorage() const { return false; }
|
||||||
|
|
||||||
@ -223,11 +228,12 @@ public:
|
|||||||
m_keyType(_keyType), m_valueType(_valueType) {}
|
m_keyType(_keyType), m_valueType(_valueType) {}
|
||||||
|
|
||||||
virtual bool operator==(Type const& _other) const override;
|
virtual bool operator==(Type const& _other) const override;
|
||||||
virtual std::string toString() const override { return "mapping(...=>...)"; }
|
virtual std::string toString() const override;
|
||||||
virtual bool canLiveOutsideStorage() const { return false; }
|
virtual bool canLiveOutsideStorage() const { return false; }
|
||||||
|
|
||||||
std::shared_ptr<Type const> getKeyType() const { return m_keyType; }
|
std::shared_ptr<Type const> getKeyType() const { return m_keyType; }
|
||||||
std::shared_ptr<Type const> getValueType() const { return m_valueType; }
|
std::shared_ptr<Type const> getValueType() const { return m_valueType; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<Type const> m_keyType;
|
std::shared_ptr<Type const> m_keyType;
|
||||||
std::shared_ptr<Type const> m_valueType;
|
std::shared_ptr<Type const> m_valueType;
|
||||||
|
Loading…
Reference in New Issue
Block a user