From 1e3037f0be430ef2339838bbdede11f45658bd82 Mon Sep 17 00:00:00 2001 From: Peter Collingbourne Date: Mon, 16 Sep 2013 01:08:15 +0000 Subject: Implement function prefix data as an IR feature. Previous discussion: http://lists.cs.uiuc.edu/pipermail/llvmdev/2013-July/063909.html Differential Revision: http://llvm-reviews.chandlerc.com/D1191 git-svn-id: https://llvm.org/svn/llvm-project/llvm/trunk@190773 91177308-0d34-0410-b5e6-96231b3b80d8 --- docs/BitCodeFormat.rst | 5 +++- docs/LangRef.rst | 54 ++++++++++++++++++++++++++++++---- include/llvm/IR/Function.h | 13 ++++++-- lib/AsmParser/LLLexer.cpp | 1 + lib/AsmParser/LLParser.cpp | 8 +++-- lib/AsmParser/LLToken.h | 1 + lib/Bitcode/Reader/BitcodeReader.cpp | 18 ++++++++++++ lib/Bitcode/Reader/BitcodeReader.h | 1 + lib/Bitcode/Writer/BitcodeWriter.cpp | 6 +++- lib/Bitcode/Writer/ValueEnumerator.cpp | 5 ++++ lib/CodeGen/AsmPrinter/AsmPrinter.cpp | 4 +++ lib/IR/AsmWriter.cpp | 4 +++ lib/IR/Function.cpp | 36 +++++++++++++++++++++++ lib/IR/LLVMContextImpl.h | 5 ++++ lib/IR/TypeFinder.cpp | 3 ++ lib/Linker/LinkModules.cpp | 17 ++++++++++- lib/Transforms/IPO/GlobalDCE.cpp | 3 ++ test/CodeGen/X86/prefixdata.ll | 15 ++++++++++ test/Feature/prefixdata.ll | 18 ++++++++++++ test/Linker/prefixdata.ll | 9 ++++++ 20 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 test/CodeGen/X86/prefixdata.ll create mode 100644 test/Feature/prefixdata.ll create mode 100644 test/Linker/prefixdata.ll diff --git a/docs/BitCodeFormat.rst b/docs/BitCodeFormat.rst index c83b6c1..d9d1df0 100644 --- a/docs/BitCodeFormat.rst +++ b/docs/BitCodeFormat.rst @@ -718,7 +718,7 @@ global variable. The operand fields are: MODULE_CODE_FUNCTION Record ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -``[FUNCTION, type, callingconv, isproto, linkage, paramattr, alignment, section, visibility, gc]`` +``[FUNCTION, type, callingconv, isproto, linkage, paramattr, alignment, section, visibility, gc, prefix]`` The ``FUNCTION`` record (code 8) marks the declaration or definition of a function. The operand fields are: @@ -757,6 +757,9 @@ function. The operand fields are: * *unnamed_addr*: If present and non-zero, indicates that the function has ``unnamed_addr`` +* *prefix*: If non-zero, the value index of the prefix data for this function, + plus 1. + MODULE_CODE_ALIAS Record ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/LangRef.rst b/docs/LangRef.rst index f961188..08874bf 100644 --- a/docs/LangRef.rst +++ b/docs/LangRef.rst @@ -552,16 +552,16 @@ an optional ``unnamed_addr`` attribute, a return type, an optional name, a (possibly empty) argument list (each with optional :ref:`parameter attributes `), optional :ref:`function attributes `, an optional section, an optional alignment, an optional :ref:`garbage -collector name `, an opening curly brace, a list of basic blocks, -and a closing curly brace. +collector name `, an optional :ref:`prefix `, an opening +curly brace, a list of basic blocks, and a closing curly brace. LLVM function declarations consist of the "``declare``" keyword, an optional :ref:`linkage type `, an optional :ref:`visibility style `, an optional :ref:`calling convention `, an optional ``unnamed_addr`` attribute, a return type, an optional :ref:`parameter attribute ` for the return type, a function -name, a possibly empty list of arguments, an optional alignment, and an -optional :ref:`garbage collector name `. +name, a possibly empty list of arguments, an optional alignment, an optional +:ref:`garbage collector name ` and an optional :ref:`prefix `. A function definition contains a list of basic blocks, forming the CFG (Control Flow Graph) for the function. Each basic block may optionally @@ -598,7 +598,7 @@ Syntax:: [cconv] [ret attrs] @ ([argument list]) [fn Attrs] [section "name"] [align N] - [gc] { ... } + [gc] [prefix Constant] { ... } .. _langref_aliases: @@ -757,6 +757,50 @@ The compiler declares the supported values of *name*. Specifying a collector which will cause the compiler to alter its output in order to support the named garbage collection algorithm. +.. _prefixdata: + +Prefix Data +----------- + +Prefix data is data associated with a function which the code generator +will emit immediately before the function body. The purpose of this feature +is to allow frontends to associate language-specific runtime metadata with +specific functions and make it available through the function pointer while +still allowing the function pointer to be called. To access the data for a +given function, a program may bitcast the function pointer to a pointer to +the constant's type. This implies that the IR symbol points to the start +of the prefix data. + +To maintain the semantics of ordinary function calls, the prefix data must +have a particular format. Specifically, it must begin with a sequence of +bytes which decode to a sequence of machine instructions, valid for the +module's target, which transfer control to the point immediately succeeding +the prefix data, without performing any other visible action. This allows +the inliner and other passes to reason about the semantics of the function +definition without needing to reason about the prefix data. Obviously this +makes the format of the prefix data highly target dependent. + +A trivial example of valid prefix data for the x86 architecture is ``i8 144``, +which encodes the ``nop`` instruction: + +.. code-block:: llvm + + define void @f() prefix i8 144 { ... } + +Generally prefix data can be formed by encoding a relative branch instruction +which skips the metadata, as in this example of valid prefix data for the +x86_64 architecture, where the first two bytes encode ``jmp .+10``: + +.. code-block:: llvm + + %0 = type <{ i8, i8, i8* }> + + define void @f() prefix %0 <{ i8 235, i8 8, i8* @md}> { ... } + +A function may have prefix data but no body. This has similar semantics +to the ``available_externally`` linkage in that the data may be used by the +optimizers but will not be emitted in the object file. + .. _attrgrp: Attribute Groups diff --git a/include/llvm/IR/Function.h b/include/llvm/IR/Function.h index 0e51c6f..bba7ecd 100644 --- a/include/llvm/IR/Function.h +++ b/include/llvm/IR/Function.h @@ -159,11 +159,11 @@ public: /// calling convention of this function. The enum values for the known /// calling conventions are defined in CallingConv.h. CallingConv::ID getCallingConv() const { - return static_cast(getSubclassDataFromValue() >> 1); + return static_cast(getSubclassDataFromValue() >> 2); } void setCallingConv(CallingConv::ID CC) { - setValueSubclassData((getSubclassDataFromValue() & 1) | - (static_cast(CC) << 1)); + setValueSubclassData((getSubclassDataFromValue() & 3) | + (static_cast(CC) << 2)); } /// @brief Return the attribute list for this Function. @@ -427,6 +427,13 @@ public: size_t arg_size() const; bool arg_empty() const; + bool hasPrefixData() const { + return getSubclassDataFromValue() & 2; + } + + Constant *getPrefixData() const; + void setPrefixData(Constant *PrefixData); + /// viewCFG - This function is meant for use from the debugger. You can just /// say 'call F->viewCFG()' and a ghostview window should pop up from the /// program, displaying the CFG of the current function with the code for each diff --git a/lib/AsmParser/LLLexer.cpp b/lib/AsmParser/LLLexer.cpp index de29c8d..99bff45 100644 --- a/lib/AsmParser/LLLexer.cpp +++ b/lib/AsmParser/LLLexer.cpp @@ -540,6 +540,7 @@ lltok::Kind LLLexer::LexIdentifier() { KEYWORD(alignstack); KEYWORD(inteldialect); KEYWORD(gc); + KEYWORD(prefix); KEYWORD(ccc); KEYWORD(fastcc); diff --git a/lib/AsmParser/LLParser.cpp b/lib/AsmParser/LLParser.cpp index 8fa0104..336518c 100644 --- a/lib/AsmParser/LLParser.cpp +++ b/lib/AsmParser/LLParser.cpp @@ -2922,7 +2922,7 @@ bool LLParser::ParseTypeAndBasicBlock(BasicBlock *&BB, LocTy &Loc, /// FunctionHeader /// ::= OptionalLinkage OptionalVisibility OptionalCallingConv OptRetAttrs /// OptUnnamedAddr Type GlobalName '(' ArgList ')' OptFuncAttrs OptSection -/// OptionalAlign OptGC +/// OptionalAlign OptGC OptionalPrefix bool LLParser::ParseFunctionHeader(Function *&Fn, bool isDefine) { // Parse the linkage. LocTy LinkageLoc = Lex.getLoc(); @@ -3001,6 +3001,7 @@ bool LLParser::ParseFunctionHeader(Function *&Fn, bool isDefine) { std::string GC; bool UnnamedAddr; LocTy UnnamedAddrLoc; + Constant *Prefix = 0; if (ParseArgumentList(ArgList, isVarArg) || ParseOptionalToken(lltok::kw_unnamed_addr, UnnamedAddr, @@ -3011,7 +3012,9 @@ bool LLParser::ParseFunctionHeader(Function *&Fn, bool isDefine) { ParseStringConstant(Section)) || ParseOptionalAlignment(Alignment) || (EatIfPresent(lltok::kw_gc) && - ParseStringConstant(GC))) + ParseStringConstant(GC)) || + (EatIfPresent(lltok::kw_prefix) && + ParseGlobalTypeAndValue(Prefix))) return true; if (FuncAttrs.contains(Attribute::Builtin)) @@ -3109,6 +3112,7 @@ bool LLParser::ParseFunctionHeader(Function *&Fn, bool isDefine) { Fn->setAlignment(Alignment); Fn->setSection(Section); if (!GC.empty()) Fn->setGC(GC.c_str()); + Fn->setPrefixData(Prefix); ForwardRefAttrGroups[Fn] = FwdRefAttrGrps; // Add all of the arguments we parsed to the function. diff --git a/lib/AsmParser/LLToken.h b/lib/AsmParser/LLToken.h index 9a6799d..e1382fd 100644 --- a/lib/AsmParser/LLToken.h +++ b/lib/AsmParser/LLToken.h @@ -81,6 +81,7 @@ namespace lltok { kw_alignstack, kw_inteldialect, kw_gc, + kw_prefix, kw_c, kw_cc, kw_ccc, kw_fastcc, kw_coldcc, diff --git a/lib/Bitcode/Reader/BitcodeReader.cpp b/lib/Bitcode/Reader/BitcodeReader.cpp index ca432fd..0c20163 100644 --- a/lib/Bitcode/Reader/BitcodeReader.cpp +++ b/lib/Bitcode/Reader/BitcodeReader.cpp @@ -1106,9 +1106,11 @@ uint64_t BitcodeReader::decodeSignRotatedValue(uint64_t V) { bool BitcodeReader::ResolveGlobalAndAliasInits() { std::vector > GlobalInitWorklist; std::vector > AliasInitWorklist; + std::vector > FunctionPrefixWorklist; GlobalInitWorklist.swap(GlobalInits); AliasInitWorklist.swap(AliasInits); + FunctionPrefixWorklist.swap(FunctionPrefixes); while (!GlobalInitWorklist.empty()) { unsigned ValID = GlobalInitWorklist.back().second; @@ -1136,6 +1138,20 @@ bool BitcodeReader::ResolveGlobalAndAliasInits() { } AliasInitWorklist.pop_back(); } + + while (!FunctionPrefixWorklist.empty()) { + unsigned ValID = FunctionPrefixWorklist.back().second; + if (ValID >= ValueList.size()) { + FunctionPrefixes.push_back(FunctionPrefixWorklist.back()); + } else { + if (Constant *C = dyn_cast(ValueList[ValID])) + FunctionPrefixWorklist.back().first->setPrefixData(C); + else + return Error("Function prefix is not a constant!"); + } + FunctionPrefixWorklist.pop_back(); + } + return false; } @@ -1881,6 +1897,8 @@ bool BitcodeReader::ParseModule(bool Resume) { if (Record.size() > 9) UnnamedAddr = Record[9]; Func->setUnnamedAddr(UnnamedAddr); + if (Record.size() > 10 && Record[10] != 0) + FunctionPrefixes.push_back(std::make_pair(Func, Record[10]-1)); ValueList.push_back(Func); // If this is a function with a body, remember the prototype we are diff --git a/lib/Bitcode/Reader/BitcodeReader.h b/lib/Bitcode/Reader/BitcodeReader.h index b095447..9533597 100644 --- a/lib/Bitcode/Reader/BitcodeReader.h +++ b/lib/Bitcode/Reader/BitcodeReader.h @@ -142,6 +142,7 @@ class BitcodeReader : public GVMaterializer { std::vector > GlobalInits; std::vector > AliasInits; + std::vector > FunctionPrefixes; /// MAttributes - The set of attributes by index. Index zero in the /// file is for null, and is thus not represented here. As such all indices diff --git a/lib/Bitcode/Writer/BitcodeWriter.cpp b/lib/Bitcode/Writer/BitcodeWriter.cpp index ed3c267..46eff4e 100644 --- a/lib/Bitcode/Writer/BitcodeWriter.cpp +++ b/lib/Bitcode/Writer/BitcodeWriter.cpp @@ -632,7 +632,7 @@ static void WriteModuleInfo(const Module *M, const ValueEnumerator &VE, // Emit the function proto information. for (Module::const_iterator F = M->begin(), E = M->end(); F != E; ++F) { // FUNCTION: [type, callingconv, isproto, linkage, paramattrs, alignment, - // section, visibility, gc, unnamed_addr] + // section, visibility, gc, unnamed_addr, prefix] Vals.push_back(VE.getTypeID(F->getType())); Vals.push_back(F->getCallingConv()); Vals.push_back(F->isDeclaration()); @@ -643,6 +643,8 @@ static void WriteModuleInfo(const Module *M, const ValueEnumerator &VE, Vals.push_back(getEncodedVisibility(F)); Vals.push_back(F->hasGC() ? GCMap[F->getGC()] : 0); Vals.push_back(F->hasUnnamedAddr()); + Vals.push_back(F->hasPrefixData() ? (VE.getValueID(F->getPrefixData()) + 1) + : 0); unsigned AbbrevToUse = 0; Stream.EmitRecord(bitc::MODULE_CODE_FUNCTION, Vals, AbbrevToUse); @@ -1863,6 +1865,8 @@ static void WriteModuleUseLists(const Module *M, ValueEnumerator &VE, WriteUseList(FI, VE, Stream); if (!FI->isDeclaration()) WriteFunctionUseList(FI, VE, Stream); + if (FI->hasPrefixData()) + WriteUseList(FI->getPrefixData(), VE, Stream); } // Write the aliases. diff --git a/lib/Bitcode/Writer/ValueEnumerator.cpp b/lib/Bitcode/Writer/ValueEnumerator.cpp index 8bac6da..a164104 100644 --- a/lib/Bitcode/Writer/ValueEnumerator.cpp +++ b/lib/Bitcode/Writer/ValueEnumerator.cpp @@ -60,6 +60,11 @@ ValueEnumerator::ValueEnumerator(const Module *M) { I != E; ++I) EnumerateValue(I->getAliasee()); + // Enumerate the prefix data constants. + for (Module::const_iterator I = M->begin(), E = M->end(); I != E; ++I) + if (I->hasPrefixData()) + EnumerateValue(I->getPrefixData()); + // Insert constants and metadata that are named at module level into the slot // pool so that the module symbol table can refer to them... EnumerateValueSymbolTable(M->getValueSymbolTable()); diff --git a/lib/CodeGen/AsmPrinter/AsmPrinter.cpp b/lib/CodeGen/AsmPrinter/AsmPrinter.cpp index a4e7808..d0173f6 100644 --- a/lib/CodeGen/AsmPrinter/AsmPrinter.cpp +++ b/lib/CodeGen/AsmPrinter/AsmPrinter.cpp @@ -459,6 +459,10 @@ void AsmPrinter::EmitFunctionHeader() { OutStreamer.EmitLabel(DeadBlockSyms[i]); } + // Emit the prefix data. + if (F->hasPrefixData()) + EmitGlobalConstant(F->getPrefixData()); + // Emit pre-function debug and/or EH information. if (DE) { NamedRegionTimer T(EHTimerName, DWARFGroupName, TimePassesIsEnabled); diff --git a/lib/IR/AsmWriter.cpp b/lib/IR/AsmWriter.cpp index f275305..6e3b853 100644 --- a/lib/IR/AsmWriter.cpp +++ b/lib/IR/AsmWriter.cpp @@ -1647,6 +1647,10 @@ void AssemblyWriter::printFunction(const Function *F) { Out << " align " << F->getAlignment(); if (F->hasGC()) Out << " gc \"" << F->getGC() << '"'; + if (F->hasPrefixData()) { + Out << " prefix "; + writeOperand(F->getPrefixData(), true); + } if (F->isDeclaration()) { Out << '\n'; } else { diff --git a/lib/IR/Function.cpp b/lib/IR/Function.cpp index bf9d949..a64a4fa 100644 --- a/lib/IR/Function.cpp +++ b/lib/IR/Function.cpp @@ -276,6 +276,9 @@ void Function::dropAllReferences() { // blockaddresses, but BasicBlock's destructor takes care of those. while (!BasicBlocks.empty()) BasicBlocks.begin()->eraseFromParent(); + + // Prefix data is stored in a side table. + setPrefixData(0); } void Function::addAttribute(unsigned i, Attribute::AttrKind attr) { @@ -351,6 +354,10 @@ void Function::copyAttributesFrom(const GlobalValue *Src) { setGC(SrcF->getGC()); else clearGC(); + if (SrcF->hasPrefixData()) + setPrefixData(SrcF->getPrefixData()); + else + setPrefixData(0); } /// getIntrinsicID - This method returns the ID number of the specified @@ -720,3 +727,32 @@ bool Function::callsFunctionThatReturnsTwice() const { return false; } + +Constant *Function::getPrefixData() const { + assert(hasPrefixData()); + const LLVMContextImpl::PrefixDataMapTy &PDMap = + getContext().pImpl->PrefixDataMap; + assert(PDMap.find(this) != PDMap.end()); + return cast(PDMap.find(this)->second->getReturnValue()); +} + +void Function::setPrefixData(Constant *PrefixData) { + if (!PrefixData && !hasPrefixData()) + return; + + unsigned SCData = getSubclassDataFromValue(); + LLVMContextImpl::PrefixDataMapTy &PDMap = getContext().pImpl->PrefixDataMap; + ReturnInst *&PDHolder = PDMap[this]; + if (PrefixData) { + if (PDHolder) + PDHolder->setOperand(0, PrefixData); + else + PDHolder = ReturnInst::Create(getContext(), PrefixData); + SCData |= 2; + } else { + delete PDHolder; + PDMap.erase(this); + SCData &= ~2; + } + setValueSubclassData(SCData); +} diff --git a/lib/IR/LLVMContextImpl.h b/lib/IR/LLVMContextImpl.h index 0c659b8..407b985 100644 --- a/lib/IR/LLVMContextImpl.h +++ b/lib/IR/LLVMContextImpl.h @@ -355,6 +355,11 @@ public: typedef DenseMap IntrinsicIDCacheTy; IntrinsicIDCacheTy IntrinsicIDCache; + /// \brief Mapping from a function to its prefix data, which is stored as the + /// operand of an unparented ReturnInst so that the prefix data has a Use. + typedef DenseMap PrefixDataMapTy; + PrefixDataMapTy PrefixDataMap; + int getOrAddScopeRecordIdxEntry(MDNode *N, int ExistingIdx); int getOrAddScopeInlinedAtIdxEntry(MDNode *Scope, MDNode *IA,int ExistingIdx); diff --git a/lib/IR/TypeFinder.cpp b/lib/IR/TypeFinder.cpp index d5e6203..dd93302 100644 --- a/lib/IR/TypeFinder.cpp +++ b/lib/IR/TypeFinder.cpp @@ -44,6 +44,9 @@ void TypeFinder::run(const Module &M, bool onlyNamed) { for (Module::const_iterator FI = M.begin(), E = M.end(); FI != E; ++FI) { incorporateType(FI->getType()); + if (FI->hasPrefixData()) + incorporateValue(FI->getPrefixData()); + // First incorporate the arguments. for (Function::const_arg_iterator AI = FI->arg_begin(), AE = FI->arg_end(); AI != AE; ++AI) diff --git a/lib/Linker/LinkModules.cpp b/lib/Linker/LinkModules.cpp index 4fffa55..c3bcbcf 100644 --- a/lib/Linker/LinkModules.cpp +++ b/lib/Linker/LinkModules.cpp @@ -1260,6 +1260,13 @@ bool ModuleLinker::run() { // Skip if not linking from source. if (DoNotLinkFromSource.count(SF)) continue; + Function *DF = cast(ValueMap[SF]); + if (SF->hasPrefixData()) { + // Link in the prefix data. + DF->setPrefixData(MapValue( + SF->getPrefixData(), ValueMap, RF_None, &TypeMap, &ValMaterializer)); + } + // Skip if no body (function is external) or materialize. if (SF->isDeclaration()) { if (!SF->isMaterializable()) @@ -1268,7 +1275,7 @@ bool ModuleLinker::run() { return true; } - linkFunctionBody(cast(ValueMap[SF]), SF); + linkFunctionBody(DF, SF); SF->Dematerialize(); } @@ -1296,6 +1303,14 @@ bool ModuleLinker::run() { continue; Function *DF = cast(ValueMap[SF]); + if (SF->hasPrefixData()) { + // Link in the prefix data. + DF->setPrefixData(MapValue(SF->getPrefixData(), + ValueMap, + RF_None, + &TypeMap, + &ValMaterializer)); + } // Materialize if necessary. if (SF->isDeclaration()) { diff --git a/lib/Transforms/IPO/GlobalDCE.cpp b/lib/Transforms/IPO/GlobalDCE.cpp index 201f320..901295d 100644 --- a/lib/Transforms/IPO/GlobalDCE.cpp +++ b/lib/Transforms/IPO/GlobalDCE.cpp @@ -179,6 +179,9 @@ void GlobalDCE::GlobalIsNeeded(GlobalValue *G) { // any globals used will be marked as needed. Function *F = cast(G); + if (F->hasPrefixData()) + MarkUsedGlobalsAsNeeded(F->getPrefixData()); + for (Function::iterator BB = F->begin(), E = F->end(); BB != E; ++BB) for (BasicBlock::iterator I = BB->begin(), E = BB->end(); I != E; ++I) for (User::op_iterator U = I->op_begin(), E = I->op_end(); U != E; ++U) diff --git a/test/CodeGen/X86/prefixdata.ll b/test/CodeGen/X86/prefixdata.ll new file mode 100644 index 0000000..2ffa89d --- /dev/null +++ b/test/CodeGen/X86/prefixdata.ll @@ -0,0 +1,15 @@ +; RUN: llc < %s -mtriple=x86_64-unknown-unknown | FileCheck %s + +@i = linkonce_odr global i32 1 + +; CHECK: f: +; CHECK-NEXT: .long 1 +define void @f() prefix i32 1 { + ret void +} + +; CHECK: g: +; CHECK-NEXT: .quad i +define void @g() prefix i32* @i { + ret void +} diff --git a/test/Feature/prefixdata.ll b/test/Feature/prefixdata.ll new file mode 100644 index 0000000..b53945c --- /dev/null +++ b/test/Feature/prefixdata.ll @@ -0,0 +1,18 @@ +; RUN: llvm-as < %s | llvm-dis > %t1.ll +; RUN: FileCheck %s < %t1.ll +; RUN: llvm-as < %t1.ll | llvm-dis > %t2.ll +; RUN: diff %t1.ll %t2.ll +; RUN: opt -O3 -S < %t1.ll | FileCheck %s + +; CHECK: @i +@i = linkonce_odr global i32 1 + +; CHECK: f(){{.*}}prefix i32 1 +define void @f() prefix i32 1 { + ret void +} + +; CHECK: g(){{.*}}prefix i32* @i +define void @g() prefix i32* @i { + ret void +} diff --git a/test/Linker/prefixdata.ll b/test/Linker/prefixdata.ll new file mode 100644 index 0000000..1f11dc7 --- /dev/null +++ b/test/Linker/prefixdata.ll @@ -0,0 +1,9 @@ +; RUN: echo > %t.ll +; RUN: llvm-link %t.ll %s -S -o - | FileCheck %s + +@i = linkonce_odr global i32 1 + +; CHECK: define void @f() prefix i32* @i +define void @f() prefix i32* @i { + ret void +} -- cgit v1.1