From 06538905915b15bde6364459929fe89cf0b8de5c Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 13:43:50 +0000 Subject: feat(codegen): arrange EXTERN signatures with clang's C ABI Every EXTERN "C" signature is now arranged by clang's CodeGen (arrangeFreeFunctionCall) for the host triple and applied by one generic lowering routine, replacing the hand-written signext/zeroext rule. clangCodeGen is linked statically into CPSCodeGen. Records still cross only through pointer types, so generated code is unchanged. Co-Authored-By: Claude Opus 5.5 --- lib/CodeGen/CABI/ClangABI.cc | 227 +++++++++++++++++++++++++++++++++++++++++++ lib/CodeGen/CABI/Lowering.cc | 140 ++++++++++++++++++++++++++ lib/CodeGen/FunctionGen.cc | 50 +++++----- 3 files changed, 393 insertions(+), 24 deletions(-) create mode 100644 lib/CodeGen/CABI/ClangABI.cc create mode 100644 lib/CodeGen/CABI/Lowering.cc (limited to 'lib/CodeGen') diff --git a/lib/CodeGen/CABI/ClangABI.cc b/lib/CodeGen/CABI/ClangABI.cc new file mode 100644 index 0000000..3757a94 --- /dev/null +++ b/lib/CodeGen/CABI/ClangABI.cc @@ -0,0 +1,227 @@ +// The only file that talks to clang. Keep it that way: clang::CodeGen clashes +// with cps::CodeGen, clang is built without RTTI (so never subclass its +// classes here), and the CodeGenerator must be destroyed before the ASTContext. +#include "cps/CABI.h" +#include "cps/TypeSystem.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "clang/Basic/Builtins.h" +#include "clang/Basic/Diagnostic.h" +#include "clang/Basic/DiagnosticOptions.h" +#include "clang/Basic/FileManager.h" +#include "clang/Basic/IdentifierTable.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Basic/TargetInfo.h" +#include "clang/CodeGen/CGFunctionInfo.h" +#include "clang/CodeGen/CodeGenABITypes.h" +#include "clang/CodeGen/ModuleBuilder.h" +#include "clang/Driver/CreateInvocationFromArgs.h" +#include "clang/Frontend/CompilerInvocation.h" +#include "llvm/IR/Module.h" +#include "llvm/Support/VirtualFileSystem.h" +#include "llvm/TargetParser/Host.h" +#include + +using namespace cps; +using clang::CodeGen::ABIArgInfo; + +// Members are destroyed in reverse order, so Gen goes before AST. +struct ClangABI::State { + const TypeSystem *Types = nullptr; + clang::DiagnosticOptions DiagOpts; + llvm::IntrusiveRefCntPtr Diags; + std::shared_ptr Invocation; + llvm::IntrusiveRefCntPtr Files; + std::unique_ptr Sources; + llvm::IntrusiveRefCntPtr Target; + std::unique_ptr Idents; + clang::SelectorTable Selectors; + clang::Builtin::Context Builtins; + std::unique_ptr AST; + std::unique_ptr Gen; + std::map Records; + + // Target and language options come from a driver invocation for the host + // triple, as the C compiler would use them (e.g. Apple's darwinpcs and the + // signedness of char). + bool init(llvm::LLVMContext &Context, std::string &Err) { + Diags = llvm::makeIntrusiveRefCnt( + clang::DiagnosticIDs::create(), DiagOpts, new clang::IgnoringDiagConsumer()); + std::string Triple = llvm::sys::getProcessTriple(); + std::string TargetArg = "--target=" + Triple; + const char *Args[] = {"clang", TargetArg.c_str(), "-fsyntax-only", "-x", "c", "cps-abi.c"}; + clang::CreateInvocationOptions Opts; + Opts.Diags = Diags; + Invocation = clang::createInvocation(Args, Opts); + if (Invocation) Target = clang::TargetInfo::CreateTargetInfo(*Diags, Invocation->getTargetOpts()); + if (!Target) { + Err = "cannot create a clang C ABI context for " + Triple; + return false; + } + clang::LangOptions &Lang = Invocation->getLangOpts(); + Target->adjust(*Diags, Lang, nullptr); + auto VFS = llvm::vfs::getRealFileSystem(); + Files = llvm::makeIntrusiveRefCnt(clang::FileSystemOptions(), VFS); + Sources = std::make_unique(*Diags, *Files); + Idents = std::make_unique(Lang); + AST = std::make_unique(Lang, *Sources, *Idents, Selectors, Builtins, + clang::TU_Complete); + AST->InitBuiltinTypes(*Target); + Gen = clang::CreateLLVMCodeGen(*Diags, "cps-abi", VFS, Invocation->getHeaderSearchOpts(), + Invocation->getPreprocessorOpts(), + Invocation->getCodeGenOpts(), Context); + Gen->Initialize(*AST); + return true; + } + + // C scalars map by name and every pointer to void *; records are built + // once and checked against cpsc's layout. + bool map(const TypeInfo *Info, clang::CanQualType &Out, std::string &Err) { + using Member = clang::CanQualType clang::ASTContext::*; + static const std::map Scalars = { + {"_Bool", &clang::ASTContext::BoolTy}, + {"char", &clang::ASTContext::CharTy}, + {"signed char", &clang::ASTContext::SignedCharTy}, + {"unsigned char", &clang::ASTContext::UnsignedCharTy}, + {"short", &clang::ASTContext::ShortTy}, + {"unsigned short", &clang::ASTContext::UnsignedShortTy}, + {"int", &clang::ASTContext::IntTy}, + {"unsigned int", &clang::ASTContext::UnsignedIntTy}, + {"long", &clang::ASTContext::LongTy}, + {"unsigned long", &clang::ASTContext::UnsignedLongTy}, + {"long long", &clang::ASTContext::LongLongTy}, + {"unsigned long long", &clang::ASTContext::UnsignedLongLongTy}, + {"float", &clang::ASTContext::FloatTy}, + {"double", &clang::ASTContext::DoubleTy}, + {"long double", &clang::ASTContext::LongDoubleTy}, + }; + if (Info && (Info->Kind == TypeKind::CPointer || Info->Kind == TypeKind::Pointer)) { + Out = AST->VoidPtrTy; + return true; + } + if (Info && Info->Kind == TypeKind::CScalar) { + auto It = Scalars.find(Info->CIdentity); + if (It != Scalars.end()) { + Out = (*AST).*(It->second); + return true; + } + } + if (!Info || !Info->isRecord()) { + Err = "no C type for " + (Info ? Info->Name : std::string("an unknown type")); + return false; + } + auto Cached = Records.find(Info); + if (Cached != Records.end()) { + Out = Cached->second; + return true; + } + clang::TranslationUnitDecl *Unit = AST->getTranslationUnitDecl(); + auto *Record = clang::RecordDecl::Create(*AST, clang::TagTypeKind::Struct, Unit, {}, {}, + &AST->Idents.get(Info->Name)); + Record->startDefinition(); + for (const auto &[Name, Field] : Info->Record.Fields) { + clang::CanQualType FieldType; + if (!map(Types->resolve(Field.TypeName), FieldType, Err)) return false; + auto *Decl = clang::FieldDecl::Create(*AST, Record, {}, {}, &AST->Idents.get(Name), + FieldType, nullptr, nullptr, false, + clang::ICIS_NoInit); + Decl->setAccess(clang::AS_public); + Record->addDecl(Decl); + } + Record->completeDefinition(); + Unit->addDecl(Record); + Out = AST->getCanonicalTagType(Record); + uint64_t CSize = AST->getTypeSizeInChars(Out).getQuantity(); + uint64_t Size = Gen->GetModule()->getDataLayout().getTypeAllocSize(Info->LLVMType); + if (CSize != Size) { + Err = "record " + Info->Name + " has " + std::to_string(Size) + " bytes in cpsc but " + + std::to_string(CSize) + " in C"; + return false; + } + Records.emplace(Info, Out); + return true; + } +}; + +namespace { + +bool translate(const ABIArgInfo &I, llvm::Type *ValueType, const std::string &What, CABIArg &Out, + std::string &Err) { + Out.ValueType = ValueType; + const char *Unsupported = nullptr; + switch (I.getKind()) { + case ABIArgInfo::Direct: + case ABIArgInfo::Extend: + Out.K = I.isDirect() ? CABIArg::Kind::Direct : CABIArg::Kind::Extend; + Out.CoerceType = I.getCoerceToType(); + Out.Offset = I.getDirectOffset(); + Out.DirectAlign = I.getDirectAlign(); + Out.Flatten = I.isDirect() && I.getCanBeFlattened() && llvm::isa(Out.CoerceType); + if (I.isExtend()) + Out.Extension = I.isSignExt() ? llvm::Attribute::SExt + : I.isZeroExt() ? llvm::Attribute::ZExt : llvm::Attribute::NoExt; + Out.InReg = I.getInReg(); + break; + case ABIArgInfo::Indirect: + Out.K = CABIArg::Kind::Indirect; + Out.ByVal = I.getIndirectByVal(); + Out.Align = I.getIndirectAlign().getQuantity(); + Out.InReg = I.getInReg(); + break; + case ABIArgInfo::Ignore: + Out.K = CABIArg::Kind::Ignore; + break; + case ABIArgInfo::IndirectAliased: Unsupported = "IndirectAliased"; break; + case ABIArgInfo::Expand: Unsupported = "Expand"; break; + case ABIArgInfo::CoerceAndExpand: Unsupported = "CoerceAndExpand"; break; + case ABIArgInfo::InAlloca: Unsupported = "InAlloca"; break; + case ABIArgInfo::TargetSpecific: Unsupported = "TargetSpecific"; break; + } + if (!Unsupported && I.getPaddingType()) Unsupported = "with a padding type"; + if (Unsupported) { + Err = "unsupported C ABI kind " + std::string(Unsupported) + " for " + What; + return false; + } + return true; +} + +} // namespace + +ClangABI::ClangABI(llvm::LLVMContext &Context, const TypeSystem &Types) + : Context(Context), Types(Types) {} + +ClangABI::~ClangABI() = default; + +bool ClangABI::arrange(const TypeInfo *Ret, const std::vector &Args, size_t Fixed, + bool Variadic, CABISignature &Sig, std::string &Err) { + if (!S) { + auto Fresh = std::make_unique(); + Fresh->Types = &Types; + if (!Fresh->init(Context, Err)) return false; + S = std::move(Fresh); + } + clang::CanQualType RetType = S->AST->VoidTy; + if (Ret && !S->map(Ret, RetType, Err)) return false; + std::vector ArgTypes(Args.size()); + for (size_t I = 0; I < Args.size(); ++I) + if (!S->map(Args[I], ArgTypes[I], Err)) return false; + clang::CodeGen::RequiredArgs Required = Variadic + ? clang::CodeGen::RequiredArgs(Fixed) : clang::CodeGen::RequiredArgs::All; + const clang::CodeGen::CGFunctionInfo &Info = clang::CodeGen::arrangeFreeFunctionCall( + S->Gen->CGM(), RetType, ArgTypes, clang::FunctionType::ExtInfo(), Required); + + Sig = CABISignature(); + Sig.Fixed = Fixed; + Sig.Variadic = Variadic; + Sig.CallingConv = Info.getEffectiveCallingConvention(); + if (!translate(Info.getReturnInfo(), Ret ? Ret->LLVMType : llvm::Type::getVoidTy(Context), + "the result", Sig.Ret, Err)) + return false; + Sig.Args.resize(Args.size()); + for (size_t I = 0; I < Args.size(); ++I) + if (!translate(Info.arguments()[I].info, Args[I]->LLVMType, + (I < Fixed ? "parameter " : "variadic argument ") + std::to_string(I + 1), + Sig.Args[I], Err)) + return false; + return true; +} diff --git a/lib/CodeGen/CABI/Lowering.cc b/lib/CodeGen/CABI/Lowering.cc new file mode 100644 index 0000000..32305ef --- /dev/null +++ b/lib/CodeGen/CABI/Lowering.cc @@ -0,0 +1,140 @@ +// Applies a clang-arranged CABISignature with no per-architecture code. Sizes +// are never computed here: the module has no DataLayout until emission, so a +// coerced value goes through memory that holds both the value and its coerced +// form. +#include "cps/CABI.h" +#include "llvm/IR/Function.h" +#include "llvm/IR/Instructions.h" + +using namespace llvm; +using namespace cps; + +namespace { + +bool isDirect(const CABIArg &A) { + return A.K == CABIArg::Kind::Direct || A.K == CABIArg::Kind::Extend; +} + +// The IR values one argument occupies: none, one, or a flattened struct's elements. +unsigned countIRArgs(const CABIArg &A) { + if (A.K == CABIArg::Kind::Ignore) return 0; + return A.Flatten ? cast(A.CoerceType)->getNumElements() : 1; +} + +void appendIRTypes(const CABIArg &A, std::vector &Out) { + if (A.K == CABIArg::Kind::Indirect) Out.push_back(PointerType::getUnqual(A.ValueType->getContext())); + else if (A.Flatten) for (Type *Element : cast(A.CoerceType)->elements()) Out.push_back(Element); + else if (isDirect(A)) Out.push_back(A.CoerceType); +} + +AllocaInst *entryAlloca(IRBuilder<> &B, Type *Ty, uint64_t MinAlign, const Twine &Name) { + Function *F = B.GetInsertBlock()->getParent(); + IRBuilder<> Entry(&F->getEntryBlock(), F->getEntryBlock().begin()); + AllocaInst *Memory = Entry.CreateAlloca(Ty, nullptr, Name); + if (MinAlign > Memory->getAlign().value()) Memory->setAlignment(Align(MinAlign)); + return Memory; +} + +// Memory for the value at offset 0 and its coerced form at A.Offset; the +// struct is at least as large and as aligned as either. +struct CoercionSlot { + AllocaInst *Value; + llvm::Value *Coerced; + Align CoercedAlign; +}; + +CoercionSlot coercionSlot(IRBuilder<> &B, const CABIArg &A) { + LLVMContext &C = A.ValueType->getContext(); + auto *Ty = StructType::get(C, {A.ValueType, ArrayType::get(Type::getInt8Ty(C), A.Offset), A.CoerceType}); + AllocaInst *Memory = entryAlloca(B, Ty, 0, "c.coerce"); + Value *Coerced = B.CreateConstInBoundsGEP1_32(Type::getInt8Ty(C), Memory, A.Offset); + return {Memory, Coerced, commonAlignment(Memory->getAlign(), A.Offset)}; +} + +AttributeSet argumentAttributes(LLVMContext &C, const CABIArg &A) { + AttrBuilder Attrs(C); + if (A.K == CABIArg::Kind::Extend) Attrs.addAttribute(A.Extension); + if (A.InReg) Attrs.addAttribute(Attribute::InReg); + if (isDirect(A) && A.DirectAlign) Attrs.addStackAlignmentAttr(A.DirectAlign); + if (A.K == CABIArg::Kind::Indirect && A.ByVal) { + Attrs.addByValAttr(A.ValueType); + Attrs.addAlignmentAttr(A.Align); + } + return AttributeSet::get(C, Attrs); +} + +} // namespace + +FunctionType *cps::getCABIFunctionType(const CABISignature &Sig) { + LLVMContext &C = Sig.Ret.ValueType->getContext(); + std::vector Params; + if (Sig.Ret.K == CABIArg::Kind::Indirect) Params.push_back(PointerType::getUnqual(C)); + for (size_t I = 0; I < Sig.Fixed; ++I) appendIRTypes(Sig.Args[I], Params); + Type *Ret = isDirect(Sig.Ret) ? Sig.Ret.CoerceType : Type::getVoidTy(C); + return FunctionType::get(Ret, Params, Sig.Variadic); +} + +AttributeList cps::getCABIAttributes(LLVMContext &C, const CABISignature &Sig) { + AttrBuilder RetAttrs(C); + std::vector Params; + if (Sig.Ret.K == CABIArg::Kind::Indirect) { + AttrBuilder SRet(C); + SRet.addStructRetAttr(Sig.Ret.ValueType); + SRet.addAlignmentAttr(Sig.Ret.Align); + if (Sig.Ret.InReg) SRet.addAttribute(Attribute::InReg); + Params.push_back(AttributeSet::get(C, SRet)); + } else if (isDirect(Sig.Ret)) { + if (Sig.Ret.K == CABIArg::Kind::Extend) RetAttrs.addAttribute(Sig.Ret.Extension); + if (Sig.Ret.InReg) RetAttrs.addAttribute(Attribute::InReg); + } + for (const CABIArg &A : Sig.Args) + Params.insert(Params.end(), countIRArgs(A), argumentAttributes(C, A)); + return AttributeList::get(C, AttributeSet(), AttributeSet::get(C, RetAttrs), Params); +} + +std::vector cps::lowerCABIArguments(IRBuilder<> &B, const CABISignature &Sig, + ArrayRef Args, Value *&SRet) { + std::vector Out; + SRet = nullptr; + if (Sig.Ret.K == CABIArg::Kind::Indirect) { + SRet = entryAlloca(B, Sig.Ret.ValueType, Sig.Ret.Align, "c.result"); + Out.push_back(SRet); + } + for (size_t I = 0; I < Args.size(); ++I) { + const CABIArg &A = Sig.Args[I]; + Value *V = Args[I]; + if (A.K == CABIArg::Kind::Ignore) continue; + if (A.K == CABIArg::Kind::Indirect) { + // byval or not, the callee receives memory of its own. + AllocaInst *Copy = entryAlloca(B, A.ValueType, A.Align, "c.arg"); + B.CreateStore(V, Copy); + Out.push_back(Copy); + continue; + } + if (!A.Flatten && !A.Offset && A.CoerceType == V->getType()) { + Out.push_back(V); + continue; + } + CoercionSlot Slot = coercionSlot(B, A); + B.CreateStore(V, Slot.Value); + if (!A.Flatten) { + Out.push_back(B.CreateAlignedLoad(A.CoerceType, Slot.Coerced, Slot.CoercedAlign)); + continue; + } + auto *Struct = cast(A.CoerceType); + for (unsigned E = 0; E < Struct->getNumElements(); ++E) + Out.push_back(B.CreateLoad(Struct->getElementType(E), B.CreateStructGEP(Struct, Slot.Coerced, E))); + } + return Out; +} + +Value *cps::lowerCABIResult(IRBuilder<> &B, const CABISignature &Sig, Value *Call, Value *SRet) { + const CABIArg &R = Sig.Ret; + if (R.ValueType->isVoidTy()) return nullptr; + if (R.K == CABIArg::Kind::Ignore) return Constant::getNullValue(R.ValueType); + if (R.K == CABIArg::Kind::Indirect) return B.CreateLoad(R.ValueType, SRet); + if (!R.Offset && R.CoerceType == R.ValueType) return Call; + CoercionSlot Slot = coercionSlot(B, R); + B.CreateAlignedStore(Call, Slot.Coerced, Slot.CoercedAlign); + return B.CreateLoad(R.ValueType, Slot.Value); +} diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index 1cbf19f..2cf6da3 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -274,7 +274,7 @@ Function *FunctionGen::emitPrototype(PrototypeAST *Proto) { } // The prototype names the exact C symbol; parameters and the result must be C -// types. Narrow integers carry the C ABI's sign/zero extension attributes. +// types. clang arranges the signature for the host C ABI. bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { if (PrototypeLinks.count(Proto)) return true; const std::string &Name = Proto->getName(); @@ -291,13 +291,6 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { return false; } - auto Extension = [](const TypeInfo *Info) { - if (Info->Kind != TypeKind::CScalar || !Info->LLVMType->isIntegerTy() || - Info->LLVMType->getIntegerBitWidth() >= 32) - return Attribute::None; - return Info->CSigned ? Attribute::SExt : Attribute::ZExt; - }; - // Records cross the C boundary only as C structs, for now through a // pointer type. What names the parameter or return type in diagnostics. auto CheckRecord = [&](const TypeInfo *Info, const std::string &What) { @@ -327,9 +320,7 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { Sig.IsExternal = true; Sig.IsVariadic = Proto->isVariadic(); - std::vector ArgTypes; - AttrBuilder RetAttrs(Context); - std::vector ParamAttrs; + std::vector ArgTypes; for (size_t I = 0; I < Proto->getParams().size(); ++I) { const ParamDecl &P = Proto->getParams()[I]; const TypeInfo *Info = Types.resolve(P.TypeName); @@ -344,15 +335,12 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { I + 1, Name.c_str(), P.TypeName.c_str()); return false; } - ArgTypes.push_back(Info->LLVMType); - AttrBuilder B(Context); - if (Attribute::AttrKind K = Extension(Info); K != Attribute::None) B.addAttribute(K); - ParamAttrs.push_back(AttributeSet::get(Context, B)); + ArgTypes.push_back(Info); ParamSig PS; PS.TypeName = P.TypeName; Sig.Params.push_back(std::move(PS)); } - Type *RetType = Type::getVoidTy(Context); + const TypeInfo *RetType = nullptr; if (!sameIdentifier(Proto->getReturnType(), "VOID")) { const TypeInfo *Info = Types.resolve(Proto->getReturnType()); if (!Info) { @@ -365,8 +353,7 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { Name.c_str(), Proto->getReturnType().c_str()); return false; } - RetType = Info->LLVMType; - if (Attribute::AttrKind K = Extension(Info); K != Attribute::None) RetAttrs.addAttribute(K); + RetType = Info; } // Repeated identical EXTERN declarations, as in C, share one symbol; each @@ -388,6 +375,7 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { reportError("Conflicting EXTERN declarations of %s", Name.c_str()); return false; } + Sig.ExternABI = First.ExternABI; Sig.ExternType = First.ExternType; Sig.ExternAttrs = First.ExternAttrs; PrototypeLinks.emplace(Proto, Name); @@ -401,10 +389,16 @@ bool FunctionGen::emitExternPrototype(PrototypeAST *Proto) { else reportError("EXTERN %s conflicts with FUNCTION/PROCEDURE %s", Name.c_str(), Name.c_str()); return false; } - Sig.ExternType = FunctionType::get(RetType, ArgTypes, Proto->isVariadic()); - Sig.ExternAttrs = AttributeList::get(Context, AttributeSet(), - AttributeSet::get(Context, RetAttrs), ParamAttrs); - Module.getOrInsertFunction(Name, Sig.ExternType); + std::string Err; + if (!ABI.arrange(RetType, ArgTypes, ArgTypes.size(), Sig.IsVariadic, Sig.ExternABI, Err)) { + reportError("Internal: cannot arrange EXTERN %s: %s", Name.c_str(), Err.c_str()); + return false; + } + Sig.ExternType = getCABIFunctionType(Sig.ExternABI); + Sig.ExternAttrs = getCABIAttributes(Context, Sig.ExternABI); + FunctionCallee Declared = Module.getOrInsertFunction(Name, Sig.ExternType); + if (auto *F = dyn_cast(Declared.getCallee())) + F->setCallingConv(Sig.ExternABI.CallingConv); PrototypeLinks.emplace(Proto, Name); Overloads.push_back(std::move(Sig)); return true; @@ -485,10 +479,18 @@ llvm::Value *FunctionGen::emitCall(const FuncSig &Sig, const std::vector IRArgs = lowerCABIArguments( + Builder, Sig.ExternABI, ArrayRef(Args).take_front(Fixed), SRet); + IRArgs.insert(IRArgs.end(), Args.begin() + Fixed, Args.end()); Value *Call = Sig.ExternType->getReturnType()->isVoidTy() - ? Exceptions.emitCall(Callee, Args) : Exceptions.emitCall(Callee, Args, "calltmp"); + ? Exceptions.emitCall(Callee, IRArgs) : Exceptions.emitCall(Callee, IRArgs, "calltmp"); cast(Call)->setAttributes(Sig.ExternAttrs); - return Call; + cast(Call)->setCallingConv(Sig.ExternABI.CallingConv); + Value *Result = lowerCABIResult(Builder, Sig.ExternABI, Call, SRet); + return Result ? Result : Call; } void FunctionGen::emitReturn(llvm::Value *RetVal) { -- cgit v1.3.1-4-g156e