From 311b8715806d21d789b414567d87652ad43ea1be Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Wed, 7 May 2025 22:29:16 +0200 Subject: [PATCH 01/14] Add temp default triple --- src/compilation/Compiler.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/compilation/Compiler.cs b/src/compilation/Compiler.cs index 907a31c..ffecd2e 100644 --- a/src/compilation/Compiler.cs +++ b/src/compilation/Compiler.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; + using Surab.Analysis; using Surab.Compilation.LLVM; @@ -34,6 +35,12 @@ public static string Compile( { // Note: We're not taking any locks (particularly read) as the compilation doesn't run in an asynchronous environment right now. + if (string.IsNullOrEmpty(targetTriple)) + { + // temp + targetTriple = "x86_64-pc-windows-msvc"; + } + // TODO: std project diags are not being reported right now var involvedProjects = project.GetInvolvedProjects(); var tasks = involvedProjects.Select(p => p.EnsureAnalyzedAsync()); From 275c349abe5ab7b7b815cbd1f2ebea38665997ec Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Wed, 7 May 2025 22:34:06 +0200 Subject: [PATCH 02/14] Fix abi classification --- src/compilation/System/Abi.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/compilation/System/Abi.cs b/src/compilation/System/Abi.cs index cc688ae..8bf3e77 100644 --- a/src/compilation/System/Abi.cs +++ b/src/compilation/System/Abi.cs @@ -1,5 +1,7 @@ using System.Diagnostics; +using Surab.Analysis; + namespace Surab.Compilation; internal enum Classification @@ -24,19 +26,23 @@ public static Classification ClassifyTypeForWindows(HirType type, Target target) var size = target.GetLayout(type).Size; Debug.Assert(!size.IsZeroSized, "Classify shouldn't be called with zero sized types."); + if (!(size.Bytes is 1 or 2 or 4 or 8)) + { + return Classification.Memory; + } + return type.GetRuntimeKind() switch { RuntimeTypeKind.Bool or RuntimeTypeKind.Char or RuntimeTypeKind.Ptr or RuntimeTypeKind.Struct - => size.Bytes switch - { - 1 or 2 or 4 or 8 => Classification.Integer, - _ => Classification.Memory, - }, + => Classification.Integer, + + RuntimeTypeKind.Numeric when type.KnownTypeTag.IsNumericInteger() + => Classification.Integer, - RuntimeTypeKind.Numeric + RuntimeTypeKind.Numeric when type.KnownTypeTag.IsNumericFloat() => Classification.SSE, _ => throw new UnreachableException(), From 46024d9e5db2a712a457921d6a80988f2569d9b6 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 18:47:56 +0200 Subject: [PATCH 03/14] Use ABIArgInfo as representation of ABI related computation --- src/compilation/Compiler.cs | 466 ++++++----------------- src/compilation/LLVM/LLVMTarget.cs | 305 +++++++++++++++ src/compilation/LLVM/LLVMTypeLowering.cs | 43 --- src/compilation/System/ABIArgInfo.cs | 45 +++ src/compilation/System/Abi.cs | 52 --- src/compilation/System/Layout.cs | 2 +- src/compilation/System/Target.cs | 39 +- 7 files changed, 492 insertions(+), 460 deletions(-) create mode 100644 src/compilation/LLVM/LLVMTarget.cs delete mode 100644 src/compilation/LLVM/LLVMTypeLowering.cs create mode 100644 src/compilation/System/ABIArgInfo.cs delete mode 100644 src/compilation/System/Abi.cs diff --git a/src/compilation/Compiler.cs b/src/compilation/Compiler.cs index ffecd2e..9276793 100644 --- a/src/compilation/Compiler.cs +++ b/src/compilation/Compiler.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.InteropServices; using Surab.Analysis; using Surab.Compilation.LLVM; @@ -300,12 +299,12 @@ private void SetupParams() // Adaptations of params inside fn body // --- - var lowering = GetLLVMLowering(t.Type); - switch (lowering.Kind) + var info = LLVMTarget.ComputeInfo(t.Type, isReturnType: false); + switch (info) { - case LLVMTypeLoweringKind.ByValue: + case DirectABIArgInfo: { - var alignment = Target.GetAbiAlignment(paramType); + var alignment = Target.GetAlignment(paramType); var ptr = LLVM.Builder.BuildAlloca(llvmType, paramName); ptr.SetAlignment(alignment.Bytes); @@ -315,19 +314,20 @@ private void SetupParams() CreateParamUnit(t.Value, unit); } break; - case LLVMTypeLoweringKind.ByCopyRef: + case IndirectABIArgInfo: { // paramValue is already a ptr. Treat it as the wrapper ptr directly. - var alignment = Target.GetAbiAlignment(t.Type); + var alignment = Target.GetAlignment(t.Type); Debug.Assert(paramValue.TypeOf.Kind is SCPPTypeKind.pointer, "Param value should already be a ptr."); var unit = LLVMUnit.Wrapped(llvmType, paramValue); CreateParamUnit(t.Value, unit); } break; - case LLVMTypeLoweringKind.AsInt: + case AsIntegerABIArgInfo: { var layout = Target.GetLayout(t.Type); var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); + // TODO: We don't use GetAbiAlignmentOfType in other places, why do we have it here? var alignment = Math.Max(layout.Alignment.Bytes, LLVM.TargetMachine.GetAbiAlignmentOfType(llvmIntType)).AsBytes(); var ptr = LLVM.Builder.BuildAlloca(llvmType, paramName); @@ -338,6 +338,25 @@ private void SetupParams() CreateParamUnit(t.Value, unit); } break; + case AsRealABIArgInfo: + { + var layout = Target.GetLayout(t.Type); + // TODO: Refactor this mapping into a place. + var llvmRealType = layout.Size.Bits switch + { + 32 => SCPPTypeRef.Float, + 64 => SCPPTypeRef.Double, + _ => throw new UnreachableException(), + }; + + // TODO: Same as in AsInteger, do we need GEP or memcpy instead here? + var ptr = LLVM.Builder.BuildAlloca(llvmType, paramName); + LLVM.Builder.BuildStore(paramValue, ptr); + var unit = LLVMUnit.Wrapped(llvmType, ptr); + + CreateParamUnit(t.Value, unit); + } + break; } llvmParamOffset++; @@ -377,7 +396,67 @@ public override void VisitReturnStmt(HirReturnStmt node) { sretPtr = _llvmFnUnit.Value.GetParam(0); } - LLVMTarget.CompileFnReturnStmt(FnValue.ReturnType, exprUnit, sretPtr); + CompileFnReturnStmt(FnValue.ReturnType, exprUnit, sretPtr); + } + } + + private void CompileFnReturnStmt(HirType returnType, LLVMUnit exprUnit, SCPPValueRef? sretOpt) + { + var info = LLVMTarget.ComputeInfo(returnType, isReturnType: true); + + if (info is IgnoreABIArgInfo) + { + return; + } + + if (info is DirectABIArgInfo) + { + var (_, value) = exprUnit.Unwrap(LLVM); + LLVM.Builder.BuildRet(value); + return; + } + + if (info is IndirectABIArgInfo) + { + // sret + Debug.Assert(sretOpt.HasValue); + var sretPtr = sretOpt.Value; + Debug.Assert(sretPtr.TypeOf.Kind is SCPPTypeKind.pointer); + var (_, exprValue) = exprUnit.Unwrap(LLVM); + LLVM.Builder.BuildStore(exprValue, sretPtr); + LLVM.Builder.BuildRetVoid(); + return; + } + + if (info is AsIntegerABIArgInfo) + { + // abi int + Debug.Assert(exprUnit.IsWrapped); + var (_, exprValuePtr) = exprUnit; + var layout = Target.GetLayout(returnType); + var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); + var loaded = LLVM.Builder.BuildLoad(llvmIntType, exprValuePtr); + loaded.SetAlignment(layout.Alignment.Bytes); + LLVM.Builder.BuildRet(loaded); + return; + } + + if (info is AsRealABIArgInfo) + { + // abi real + Debug.Assert(exprUnit.IsWrapped); + var (_, exprValuePtr) = exprUnit; + var layout = Target.GetLayout(returnType); + var llvmRealType = layout.Size.Bits switch + { + 32 => SCPPTypeRef.Float, + 64 => SCPPTypeRef.Double, + _ => throw new UnreachableException(), + }; + var loaded = LLVM.Builder.BuildLoad(llvmRealType, exprValuePtr); + loaded.SetAlignment(layout.Alignment.Bytes); + LLVM.Builder.BuildRet(loaded); + return; } } @@ -419,16 +498,16 @@ public override void VisitCallExpr(HirCallExpr node) // --- var paramType = fnType.ParamTypes[argOffset]; - var lowering = GetLLVMLowering(paramType); - switch (lowering.Kind) + var info = LLVMTarget.ComputeInfo(paramType, isReturnType: false); + switch (info) { - case LLVMTypeLoweringKind.ByValue: + case DirectABIArgInfo: { var (_, argValue) = argUnit.Unwrap(LLVM); args.Add(argValue); } break; - case LLVMTypeLoweringKind.ByCopyRef: + case IndirectABIArgInfo: { var llvmParamType = LLVMTarget.LowerType(paramType); // Copy the value into a new ptr to preserve *pass by copy* semantics. @@ -438,7 +517,7 @@ public override void VisitCallExpr(HirCallExpr node) args.Add(copyPtr); } break; - case LLVMTypeLoweringKind.AsInt: + case AsIntegerABIArgInfo: { // Might not be right, so this assertion is to detect a case where this isn't // true so we can look into it when it happens. @@ -447,6 +526,24 @@ public override void VisitCallExpr(HirCallExpr node) var layout = Target.GetLayout(paramType); var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); var loaded = LLVM.Builder.BuildLoad(llvmIntType, argValuePtr); + // TODO: In SetupParams, we're computing the max between this and the llvm type alignment. + // Which one is right? + loaded.SetAlignment(layout.Alignment.Bytes); + args.Add(loaded); + } + break; + case AsRealABIArgInfo: + { + Debug.Assert(argUnit.IsWrapped); + var (_, argValuePtr) = argUnit; + var layout = Target.GetLayout(paramType); + var llvmRealType = layout.Size.Bits switch + { + 32 => SCPPTypeRef.Float, + 64 => SCPPTypeRef.Double, + _ => throw new UnreachableException(), + }; + var loaded = LLVM.Builder.BuildLoad(llvmRealType, argValuePtr); loaded.SetAlignment(layout.Alignment.Bytes); args.Add(loaded); } @@ -693,7 +790,7 @@ private void GenerateValueStore(LLVMUnit destUnit, HirType destType, LLVMUnit to if (destType is HirStructType && toType is HirStructType && toUnit.IsWrapped) { var (_, rhs) = toUnit; - var size = Target.GetAbiSize(toType); + var size = Target.GetSize(toType); LLVM.CallIntrinsic_memcpy_i64(value, rhs, size.Bytes); } else @@ -741,7 +838,7 @@ public override void VisitFieldStoreExpr(HirFieldStoreStmt node) if (leftType is HirStructType && rightType is HirStructType && rhsUnit.IsWrapped) { var (_, rhs) = rhsUnit; - var size = Target.GetAbiSize(node.Expr.Type); + var size = Target.GetSize(node.Expr.Type); LLVM.CallIntrinsic_memcpy_i64(ptrToField, rhs, size.Bytes); } else @@ -873,341 +970,4 @@ private LLVMUnit CreateFnUnit(HirFnValue fnValue) // Create an unwrapped unit out of direct fn values. This means direct calls to this fn value won't be loaded (in VisitCallExpr). return LLVMUnit.Unwrapped(llvmPtrType, llvmFnValue); } - - public LLVMTypeLowering GetLLVMLowering(HirType type) - { - return LLVMTarget.GetLLVMLowering(type); - } -} - -/// -/// LLVM compilation related logic that changes with target. -/// -internal abstract class LLVMTarget -{ - protected LLVMTarget(CompilationContext context) - { - Context = context; - } - - protected CompilationContext Context { get; } - - protected LLVMCompilationContext LLVM => Context.LLVM; - - protected Target Target => Context.Target; - - public static LLVMTarget Create(CompilationContext context) - { - var target = context.Target; - - return target switch - { - x86_64WindowsMSVCTarget => new x86_64WindowsMSVCLLVMTarget(context), - _ => throw new UnreachableException(), - }; - } - - public bool IsReturnSRet(HirFnType fnType) - { - if (Target.GetAbiSize(fnType.ReturnType).IsZeroSized) - return false; - - return IsReturnSRetCore(fnType); - } - - public abstract bool IsReturnSRetCore(HirFnType fnType); - - // TODO: Support different calling conventions. Right now it's always Win64 cc. - public SCPPTypeRef LowerFnReturnType(HirFnType fnType) - { - if (fnType.ReturnType.KnownTypeTag == KnownTypeTag.@void) - return SCPPTypeRef.Void; - - Debug.Assert(!Target.GetAbiSize(fnType.ReturnType).IsZeroSized); - - return LowerFnReturnTypeCore(fnType); - } - - protected abstract SCPPTypeRef LowerFnReturnTypeCore(HirFnType fnType); - - public void CompileFnReturnStmt(HirType returnType, LLVMUnit exprUnit, SCPPValueRef? sretOpt) - { - CompileFnReturnStmtCore(returnType, exprUnit, sretOpt); - } - - protected abstract void CompileFnReturnStmtCore(HirType returnType, LLVMUnit exprUnit, SCPPValueRef? sretOpt); - - /// - /// Lowers a type from Surab to LLVM. - /// - /// The Surab type to lower. - /// The lowered LLVM type. - public SCPPTypeRef LowerType(HirType type) - { - if (Context.TypeToLLVMType.TryGetValue(type, out var llvmType)) - { - return llvmType; - } - - llvmType = LowerTypeCore(type); - Context.TypeToLLVMType[type] = llvmType; - return llvmType; - } - - private SCPPTypeRef LowerTypeCore(HirType type) - { - if (type.Kind is HirTypeKind.GenericParam) - { - throw new Exception("Unexpected generic param type still present when lowering type to LLVM."); - } - - var tag = type.KnownTypeTag; - - if (tag.IsNumericInteger()) - { - var size = Target.GetAbiSize(type); - return SCPPTypeRef.CreateInt(size.Bits); - } - - if (tag is KnownTypeTag.@void) - return SCPPTypeRef.Void; - if (tag is KnownTypeTag.@char or KnownTypeTag.@bool) - return SCPPTypeRef.Int1; - - if (tag is KnownTypeTag.f32) - return SCPPTypeRef.Float; - if (tag is KnownTypeTag.f64) - return SCPPTypeRef.Double; - - if (tag is KnownTypeTag.@string || type.Kind is HirTypeKind.Ptr) - return SCPPTypeRef.CreatePointer(SCPPTypeRef.Void, 0); - - if (type is HirStructType structType) - { - var fields = structType.Fields; - var structLayout = Target.GetStructLayout(structType); - var elements = structLayout.Sections.Select(m => - { - return m.Kind switch - { - StructLayoutSectionKind.Padding => SCPPTypeRef.CreateArray(SCPPTypeRef.Int1, m.Size.Bytes), - StructLayoutSectionKind.Data => LowerType(fields[m.FieldIndex].Type), - _ => throw new UnreachableException(), - }; - }).ToArray(); - // TODO: Optionally mangled name instead. - var llvmType = SCPPTypeRef.CreateNamedStruct(structType.FullName); - llvmType.StructSetBody(elements, packed: false); - return llvmType; - } - - if (type is HirFnType fnType) - { - var (llvmType, _) = LowerType(fnType); - return llvmType; - } - - throw new UnreachableException(); - } - - /// - /// Lowers an fn type from Surab to LLVM. - /// - /// The Surab fn type to lower. - /// The lowered LLVM type (which is an opaque ptr in the case of fns here) and the actual LLVM type (the actual fn type). - public (SCPPTypeRef llvmType, SCPPTypeRef llvmActualType) LowerType(HirFnType fnType) - { - var (sret, _) = GetFnInfoFromFnType(fnType); - var returnType = LowerFnReturnType(fnType); - - // TODO: Better way? - var llvmIndex = 0; - var capacity = fnType.ParamTypes.Count + 1; // Max length of llvm params. - var @params = new List(capacity); - - if (sret.HasValue) - { - @params.Add(SCPPTypeRef.CreatePointer(LowerType(fnType.ReturnType), 0)); - llvmIndex++; - } - - foreach (var paramType in fnType.ParamTypes) - { - // Create fn param types - // --- - - var lowering = GetLLVMLowering(paramType); - var paramLLVMType = LowerType(paramType); - switch (lowering.Kind) - { - case LLVMTypeLoweringKind.ByValue: - { - @params.Add(paramLLVMType); - } - break; - case LLVMTypeLoweringKind.ByCopyRef: - { - @params.Add(SCPPTypeRef.CreatePointer(paramLLVMType, 0)); - } - break; - case LLVMTypeLoweringKind.AsInt: - { - var layout = Target.GetLayout(paramType); - var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); - @params.Add(llvmIntType); - } - break; - } - - llvmIndex++; - } - - // SCPPTypeRef.CreateFunctionType - var llvmFnType = SCPPTypeRef.CreateFunctionType(returnType, CollectionsMarshal.AsSpan(@params), false); - - // TODO: AddressSpace? - return (SCPPTypeRef.CreatePointer(llvmFnType, 0), llvmFnType); - } - - // TODO: Optimize calls to this method? This is being called multiple times in sequence when we compile an fn. We could cache the result. - public LLVMFn GetFnInfoFromFnType(HirFnType fnType) - { - //Debug.Assert(fnType is HirFnType fnType); - var returnType = LowerType(fnType.ReturnType); - - // TODO: Better way? - var llvmIndex = 0; - var attrs = new SCPPFnAttrsDef(); - - var sret = IsReturnSRet(fnType); - if (sret) - { - attrs.AddParam(llvmIndex, SCPPAttributeRef.CreateTypeAttr(SCPPAttributeKind.StructRet, returnType)); - llvmIndex++; - } - - foreach (var paramType in fnType.ParamTypes) - { - // Add attrs - // --- - - var lowering = GetLLVMLowering(paramType); - //var llvmType = Target.LowerType(paramType); - switch (lowering.Kind) - { - case LLVMTypeLoweringKind.ByValue: - { - } - break; - case LLVMTypeLoweringKind.ByCopyRef: - { - } - break; - case LLVMTypeLoweringKind.AsInt: - { - } - break; - } - - llvmIndex++; - } - - return new LLVMFn(sret ? returnType : (SCPPTypeRef?)null, attrs); - } - - public LLVMTypeLowering GetLLVMLowering(HirType type) - { - if (Target.GetAbiSize(type).IsZeroSized) - return LLVMTypeLowering.ZeroSized; - - return GetLLVMLoweringCore(type); - } - - protected abstract LLVMTypeLowering GetLLVMLoweringCore(HirType type); -} - -internal abstract class x86_64WindowsLLVMTarget(CompilationContext context) : LLVMTarget(context) -{ - protected new x86_64WindowsTarget Target => (x86_64WindowsTarget)base.Target; - - public override bool IsReturnSRetCore(HirFnType fnType) - { - return Target.Classify(fnType.ReturnType) == Classification.Memory; - } - - protected override SCPPTypeRef LowerFnReturnTypeCore(HirFnType fnType) - { - var returnType = fnType.ReturnType; - var classification = Target.Classify(returnType); - return classification switch - { - Classification.Integer => returnType.IsScalar() ? LowerType(returnType) : SCPPTypeRef.CreateInt(Target.GetAbiSize(returnType).Bits), - //win_i128 => - Classification.Memory => SCPPTypeRef.Void, - Classification.SSE => LowerType(returnType), - _ => throw new UnreachableException(), - }; - } - - // Logic sync'd with LowerFnReturnTypeCore above. - protected override void CompileFnReturnStmtCore(HirType returnType, LLVMUnit exprUnit, SCPPValueRef? sretOpt) - { - Debug.Assert(!Target.GetAbiSize(returnType).IsZeroSized); - - var classification = Target.Classify(returnType); - - var passthroughCase = - (classification is Classification.Integer && returnType.IsScalar()) || - classification is Classification.SSE; - if (passthroughCase) - { - var (_, value) = exprUnit.Unwrap(LLVM); - LLVM.Builder.BuildRet(value); - return; - } - - if (classification is Classification.Memory) - { - // sret - Debug.Assert(sretOpt.HasValue); - var sretPtr = sretOpt.Value; - Debug.Assert(sretPtr.TypeOf.Kind is SCPPTypeKind.pointer); - var (_, exprValue) = exprUnit.Unwrap(LLVM); - LLVM.Builder.BuildStore(exprValue, sretPtr); - LLVM.Builder.BuildRetVoid(); - return; - } - - if (classification is Classification.Integer) - { - // abi int - Debug.Assert(exprUnit.IsWrapped); - var (_, exprValuePtr) = exprUnit; - var layout = Target.GetLayout(returnType); - var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); - var loaded = LLVM.Builder.BuildLoad(llvmIntType, exprValuePtr); - loaded.SetAlignment(layout.Alignment.Bytes); - LLVM.Builder.BuildRet(loaded); - return; - } - } - - protected override LLVMTypeLowering GetLLVMLoweringCore(HirType type) - { - Debug.Assert(!Target.GetAbiSize(type).IsZeroSized); - - return Target.Classify(type) switch - { - Classification.Integer => type.IsScalar() ? LLVMTypeLowering.ByValue : LLVMTypeLowering.AsInt, - //win_i128 => - Classification.Memory => LLVMTypeLowering.ByCopyRef, - Classification.SSE => LLVMTypeLowering.ByValue, - _ => throw new UnreachableException(), - }; - } -} - -internal sealed class x86_64WindowsMSVCLLVMTarget(CompilationContext context) : x86_64WindowsLLVMTarget(context) -{ - private new x86_64WindowsMSVCTarget Target => (x86_64WindowsMSVCTarget)Context.Target; } diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs new file mode 100644 index 0000000..6883ef6 --- /dev/null +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -0,0 +1,305 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +using Surab.Analysis; +using Surab.Compilation.LLVM; + +namespace Surab.Compilation; + +/// +/// LLVM compilation related logic that changes with target. +/// +internal abstract class LLVMTarget +{ + protected LLVMTarget(CompilationContext context) + { + Context = context; + } + + protected CompilationContext Context { get; } + + protected LLVMCompilationContext LLVM => Context.LLVM; + + protected Target Target => Context.Target; + + public static LLVMTarget Create(CompilationContext context) + { + var target = context.Target; + + return target switch + { + x86_64WindowsMSVCTarget => new x86_64WindowsMSVCLLVMTarget(context), + // TODO: linux + _ => throw new UnreachableException(), + }; + } + + // TODO: Support overriding calling convention on the fn. Right now it's always cc c. + public SCPPTypeRef LowerFnReturnType(HirFnType fnType) + { + var returnType = fnType.ReturnType; + var info = ComputeInfo(returnType, isReturnType: true); + return info switch + { + DirectABIArgInfo => LowerType(returnType), + IgnoreABIArgInfo or IndirectABIArgInfo => SCPPTypeRef.Void, + AsIntegerABIArgInfo => SCPPTypeRef.CreateInt(Target.GetSize(returnType).Bits), + AsRealABIArgInfo => Target.GetSize(returnType).Bits switch + { + 32 => SCPPTypeRef.Float, + 64 => SCPPTypeRef.Double, + _ => throw new UnreachableException(), + }, + _ => throw new UnreachableException(), + }; + } + + /// + /// Lowers a type from Surab to LLVM. + /// + /// The Surab type to lower. + /// The lowered LLVM type. + public SCPPTypeRef LowerType(HirType type) + { + if (Context.TypeToLLVMType.TryGetValue(type, out var llvmType)) + { + return llvmType; + } + + llvmType = LowerTypeCore(type); + Context.TypeToLLVMType[type] = llvmType; + return llvmType; + } + + private SCPPTypeRef LowerTypeCore(HirType type) + { + if (type.Kind is HirTypeKind.GenericParam) + { + throw new Exception("Unexpected generic param type still present when lowering type to LLVM."); + } + + var tag = type.KnownTypeTag; + + if (tag.IsNumericInteger()) + { + var size = Target.GetSize(type); + return SCPPTypeRef.CreateInt(size.Bits); + } + + if (tag is KnownTypeTag.@void) + return SCPPTypeRef.Void; + if (tag is KnownTypeTag.@char or KnownTypeTag.@bool) + return SCPPTypeRef.Int1; + + if (tag is KnownTypeTag.f32) + return SCPPTypeRef.Float; + if (tag is KnownTypeTag.f64) + return SCPPTypeRef.Double; + + if (tag is KnownTypeTag.@string || type.Kind is HirTypeKind.Ptr) + return SCPPTypeRef.CreatePointer(SCPPTypeRef.Void, 0); + + if (type is HirStructType structType) + { + var fields = structType.Fields; + var structLayout = Target.GetStructLayout(structType); + var elements = structLayout.Sections.Select(m => + { + return m.Kind switch + { + StructLayoutSectionKind.Padding => SCPPTypeRef.CreateArray(SCPPTypeRef.Int1, m.Size.Bytes), + StructLayoutSectionKind.Data => LowerType(fields[m.FieldIndex].Type), + _ => throw new UnreachableException(), + }; + }).ToArray(); + // TODO: Optionally mangled name instead. + var llvmType = SCPPTypeRef.CreateNamedStruct(structType.FullName); + llvmType.StructSetBody(elements, packed: false); + return llvmType; + } + + if (type is HirFnType fnType) + { + var (llvmType, _) = LowerType(fnType); + return llvmType; + } + + throw new UnreachableException(); + } + + /// + /// Lowers an fn type from Surab to LLVM. + /// + /// The Surab fn type to lower. + /// The lowered LLVM type (which is an opaque ptr in the case of fns here) and the actual LLVM type (the actual fn type). + public (SCPPTypeRef llvmType, SCPPTypeRef llvmActualType) LowerType(HirFnType fnType) + { + var (sret, _) = GetFnInfoFromFnType(fnType); + var returnType = LowerFnReturnType(fnType); + + // TODO: Better way? + var llvmIndex = 0; + var capacity = fnType.ParamTypes.Count + 1; // Max length of llvm params. + var @params = new List(capacity); + + if (sret.HasValue) + { + @params.Add(SCPPTypeRef.CreatePointer(LowerType(fnType.ReturnType), 0)); + llvmIndex++; + } + + foreach (var paramType in fnType.ParamTypes) + { + // Create fn param types + // --- + + var info = ComputeInfo(paramType, isReturnType: false); + var paramLLVMType = LowerType(paramType); + switch (info) + { + case DirectABIArgInfo: + { + @params.Add(paramLLVMType); + } + break; + case IndirectABIArgInfo: + { + @params.Add(SCPPTypeRef.CreatePointer(paramLLVMType, 0)); + } + break; + case AsIntegerABIArgInfo: + { + var layout = Target.GetLayout(paramType); + var llvmIntType = SCPPTypeRef.CreateInt(layout.Size.Bits); + @params.Add(llvmIntType); + } + break; + case AsRealABIArgInfo: + { + var layout = Target.GetLayout(paramType); + var llvmRealType = layout.Size.Bits switch + { + 32 => SCPPTypeRef.Float, + 64 => SCPPTypeRef.Double, + _ => throw new UnreachableException(), + }; + @params.Add(llvmRealType); + } + break; + } + + llvmIndex++; + } + + var llvmFnType = SCPPTypeRef.CreateFunctionType(returnType, CollectionsMarshal.AsSpan(@params), false); + + return (SCPPTypeRef.CreatePointer(llvmFnType, 0), llvmFnType); + } + + // TODO: Optimize calls to this method? This is being called multiple times in sequence when we compile an fn. We could cache the result. + public LLVMFn GetFnInfoFromFnType(HirFnType fnType) + { + //Debug.Assert(fnType is HirFnType fnType); + var returnType = LowerType(fnType.ReturnType); + + // TODO: Better way? + var llvmIndex = 0; + var attrs = new SCPPFnAttrsDef(); + + var sret = IsReturnSRet(fnType); + if (sret) + { + attrs.AddParam(llvmIndex, SCPPAttributeRef.CreateTypeAttr(SCPPAttributeKind.StructRet, returnType)); + llvmIndex++; + } + + foreach (var paramType in fnType.ParamTypes) + { + // Add attrs + // --- + + var info = ComputeInfo(paramType, isReturnType: false); + //var llvmType = Target.LowerType(paramType); + switch (info) + { + case DirectABIArgInfo: + { + } + break; + case IndirectABIArgInfo: + { + } + break; + case AsIntegerABIArgInfo: + { + } + break; + case AsRealABIArgInfo: + { + } + break; + } + + llvmIndex++; + } + + return new LLVMFn(sret ? returnType : (SCPPTypeRef?)null, attrs); + } + + private bool IsReturnSRet(HirFnType fnType) + { + return ComputeInfo(fnType.ReturnType, isReturnType: true) is IndirectABIArgInfo; + } + + /// + /// Computes the ABI arg info for the type. + /// + /// In general, we only need to compute enough info to know how to correctly compile to LLVM IR. + /// We don't need to fully implement the ABI's specification because a big part of that is + /// handled by LLVM itself. + /// + public abstract ABIArgInfo ComputeInfo(HirType type, bool isReturnType); +} + +// https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention +internal abstract class x86_64WindowsLLVMTarget(CompilationContext context) : LLVMTarget(context) +{ + protected new x86_64WindowsTarget Target => (x86_64WindowsTarget)base.Target; + + public override ABIArgInfo ComputeInfo(HirType type, bool isReturnType) + { + var size = Target.GetLayout(type).Size; + if (size.IsZeroSized) + { + return new IgnoreABIArgInfo(); + } + + if (!(size.Bytes is 1 or 2 or 4 or 8)) + { + return new IndirectABIArgInfo(); + } + + return type.GetRuntimeKind() switch + { + RuntimeTypeKind.Bool or + RuntimeTypeKind.Char or + RuntimeTypeKind.Ptr + => new DirectABIArgInfo(), + + RuntimeTypeKind.Numeric + => new DirectABIArgInfo(), + + RuntimeTypeKind.Struct + => new AsIntegerABIArgInfo(), + + _ => throw new UnreachableException(), + }; + } +} + +internal sealed class x86_64WindowsMSVCLLVMTarget(CompilationContext context) : x86_64WindowsLLVMTarget(context) +{ + private new x86_64WindowsMSVCTarget Target => (x86_64WindowsMSVCTarget)Context.Target; +} + +// https://gitlab.com/x86-psABIs/x86-64-ABI diff --git a/src/compilation/LLVM/LLVMTypeLowering.cs b/src/compilation/LLVM/LLVMTypeLowering.cs deleted file mode 100644 index c28b1d8..0000000 --- a/src/compilation/LLVM/LLVMTypeLowering.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Surab.Compilation.LLVM; - -/// -/// The kind of an . -/// -internal enum LLVMTypeLoweringKind -{ - None, - - /// - /// Zero sized. - /// - ZeroSized, - /// - /// By value. The arg should be passed as is by its value without any conversion. - /// - ByValue, - /// - /// By copy ref. The arg is originally passed as a copy but the abi is telling us to pass it by ref. - /// Mutations shouldn't be observed after the function we're going to call returns to preserve by copy semantics. - /// - ByCopyRef, - /// - /// As an integer. The arg should be passed as a bitcasted integer of the same bit size. - /// - AsInt, -} - -/// -/// Represents a certain conversion imposed by an ABI by which an arg should be passed. -/// -internal readonly struct LLVMTypeLowering -{ - public required LLVMTypeLoweringKind Kind { get; init; } - - public static readonly LLVMTypeLowering ZeroSized = new() { Kind = LLVMTypeLoweringKind.ZeroSized }; - - public static readonly LLVMTypeLowering ByValue = new() { Kind = LLVMTypeLoweringKind.ByValue }; - - public static readonly LLVMTypeLowering ByCopyRef = new() { Kind = LLVMTypeLoweringKind.ByCopyRef }; - - public static readonly LLVMTypeLowering AsInt = new() { Kind = LLVMTypeLoweringKind.AsInt }; -} diff --git a/src/compilation/System/ABIArgInfo.cs b/src/compilation/System/ABIArgInfo.cs new file mode 100644 index 0000000..161db0e --- /dev/null +++ b/src/compilation/System/ABIArgInfo.cs @@ -0,0 +1,45 @@ +namespace Surab.Compilation; + +/// +/// Represents ABI info for a return type or arg type on how to lower it to llvm +/// while respecting the target ABI. +/// +public abstract class ABIArgInfo +{ +} + +/// +/// Zero sized. +/// +public sealed class IgnoreABIArgInfo : ABIArgInfo +{ +} + +/// +/// Direct, by value. The arg should be passed as is by its value without any conversion. +/// +public sealed class DirectABIArgInfo : ABIArgInfo +{ +} + +/// +/// Indirect, by copy ref. The arg is originally passed as a copy but the abi is telling us to pass it by ref. +/// Mutations shouldn't be observed after the function we're going to call returns to preserve by copy semantics. +/// +public sealed class IndirectABIArgInfo : ABIArgInfo +{ +} + +/// +/// As an integer. The arg should be passed as a bitcasted integer of the same bit size. +/// +public sealed class AsIntegerABIArgInfo : ABIArgInfo +{ +} + +/// +/// As a float. The arg should be passed as a float (usually a transparent struct of a float or double). +/// +public sealed class AsRealABIArgInfo : ABIArgInfo +{ +} diff --git a/src/compilation/System/Abi.cs b/src/compilation/System/Abi.cs deleted file mode 100644 index 8bf3e77..0000000 --- a/src/compilation/System/Abi.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics; - -using Surab.Analysis; - -namespace Surab.Compilation; - -internal enum Classification -{ - // x86_64 - Integer, - // x86_64 - SSE, - // x86_64 - SSEUP, - // x86_64 - Memory, -} - -internal static class Abi -{ - public static class x86_64 - { - // https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions - public static Classification ClassifyTypeForWindows(HirType type, Target target) - { - var size = target.GetLayout(type).Size; - Debug.Assert(!size.IsZeroSized, "Classify shouldn't be called with zero sized types."); - - if (!(size.Bytes is 1 or 2 or 4 or 8)) - { - return Classification.Memory; - } - - return type.GetRuntimeKind() switch - { - RuntimeTypeKind.Bool or - RuntimeTypeKind.Char or - RuntimeTypeKind.Ptr or - RuntimeTypeKind.Struct - => Classification.Integer, - - RuntimeTypeKind.Numeric when type.KnownTypeTag.IsNumericInteger() - => Classification.Integer, - - RuntimeTypeKind.Numeric when type.KnownTypeTag.IsNumericFloat() - => Classification.SSE, - - _ => throw new UnreachableException(), - }; - } - } -} diff --git a/src/compilation/System/Layout.cs b/src/compilation/System/Layout.cs index aa3180a..8c88548 100644 --- a/src/compilation/System/Layout.cs +++ b/src/compilation/System/Layout.cs @@ -43,7 +43,7 @@ private StructLayoutSection(StructLayoutSectionKind kind) public required BitSize Size { get; init; } /// - /// Gets the index of the field in the struct this data section is associated with. + /// Gets the index of the in the struct this data section is associated with. /// This is -1 when Kind is Padding. /// public required int FieldIndex { get; init; } diff --git a/src/compilation/System/Target.cs b/src/compilation/System/Target.cs index 0f049b1..a9563b7 100644 --- a/src/compilation/System/Target.cs +++ b/src/compilation/System/Target.cs @@ -1,4 +1,5 @@ using System.Diagnostics; + using Surab.Analysis; namespace Surab.Compilation; @@ -11,11 +12,13 @@ public enum TargetArch public enum TargetOS { Windows, + Linux, } public enum TargetEnv { MSVC, + GNU, } public enum IntegerSign @@ -34,7 +37,6 @@ public abstract class Target(TargetArch arch, TargetOS os, TargetEnv env) public BitSize PtrWidth { get; protected set; } - // Should this be in Abi? public Layout GetLayout(HirType type) { Debug.Assert(type.Kind is not HirTypeKind.GenericParam); @@ -65,7 +67,6 @@ public Layout GetLayout(HirType type) throw new UnreachableException(); } - // Should this be in Abi? public StructLayout GetStructLayout(HirStructType structType) { ArgumentNullException.ThrowIfNull(structType); @@ -111,9 +112,9 @@ void AddPaddingIfNecessary(BitSize alignment) } } - public BitSize GetAbiSize(HirType type) => GetLayout(type).Size; + public BitSize GetSize(HirType type) => GetLayout(type).Size; - public BitSize GetAbiAlignment(HirType type) => GetLayout(type).Alignment; + public BitSize GetAlignment(HirType type) => GetLayout(type).Alignment; public IntegerSign GetIntegerSign(HirType type) { @@ -128,23 +129,39 @@ public IntegerSign GetIntegerSign(HirType type) } } -internal abstract class x86_64WindowsTarget : Target +internal abstract class x86_64Target : Target { - protected x86_64WindowsTarget(TargetEnv env) : base(TargetArch.x86_64, TargetOS.Windows, env) + protected x86_64Target(TargetOS os, TargetEnv env) : base(TargetArch.x86_64, os, env) { + PtrWidth = 64.AsBits(); } +} - public Classification Classify(HirType type) +internal abstract class x86_64WindowsTarget : x86_64Target +{ + protected x86_64WindowsTarget(TargetEnv env) : base(TargetOS.Windows, env) { - return Abi.x86_64.ClassifyTypeForWindows(type, this); + PtrWidth = 64.AsBits(); } } internal sealed class x86_64WindowsMSVCTarget : x86_64WindowsTarget { - public x86_64WindowsMSVCTarget() : - base(TargetEnv.MSVC) + public x86_64WindowsMSVCTarget() : base(TargetEnv.MSVC) + { + } +} + +internal abstract class x86_64LinuxTarget : x86_64Target +{ + protected x86_64LinuxTarget(TargetEnv env) : base(TargetOS.Linux, env) + { + } +} + +internal sealed class x86_64LinuxGNUTarget : x86_64LinuxTarget +{ + public x86_64LinuxGNUTarget() : base(TargetEnv.GNU) { - PtrWidth = 64.AsBits(); } } From f320e851f7478c0ccb6e414300ecc182d6525860 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 18:48:21 +0200 Subject: [PATCH 04/14] Return targets based on triple --- src/compilation/Compiler.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/compilation/Compiler.cs b/src/compilation/Compiler.cs index 9276793..680e013 100644 --- a/src/compilation/Compiler.cs +++ b/src/compilation/Compiler.cs @@ -112,7 +112,18 @@ private string Compile() private static Target ParseTargetTriple(string targetTriple) { // TODO - return new x86_64WindowsMSVCTarget(); + if (targetTriple == "x86_64-pc-windows-msvc") + { + return new x86_64WindowsMSVCTarget(); + } + else if (targetTriple == "x86_64-pc-linux-gnu") + { + return new x86_64LinuxGNUTarget(); + } + else + { + throw new UnreachableException(); + } } } From dd4aa87927872dd4193f61dc915fde087a374efc Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 18:48:37 +0200 Subject: [PATCH 05/14] Implement llvm target for linux --- src/compilation/Compiler.cs | 4 + src/compilation/LLVM/LLVMTarget.cs | 306 +++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) diff --git a/src/compilation/Compiler.cs b/src/compilation/Compiler.cs index 680e013..6a488e6 100644 --- a/src/compilation/Compiler.cs +++ b/src/compilation/Compiler.cs @@ -974,6 +974,10 @@ private LLVMUnit CreateFnUnit(HirFnValue fnValue) } // TODO: switch (cc) (C this is already the LLVM default if not set) + // Unless overriden in a function attribute: + // - If target platform is windows x64 => win64 + // - If target platform is linux x64 => c + // Note: c == win64 if host platform is windows x64 llvmFnValue.SetFunctionCallConv(CallConv.C); attrs.Apply(llvmFnValue.AddAttributeAtIndex); diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs index 6883ef6..87d1bdb 100644 --- a/src/compilation/LLVM/LLVMTarget.cs +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -303,3 +303,309 @@ internal sealed class x86_64WindowsMSVCLLVMTarget(CompilationContext context) : } // https://gitlab.com/x86-psABIs/x86-64-ABI +internal abstract class x86_64LinuxLLVMTarget(CompilationContext context) : LLVMTarget(context) +{ + // AMD64 ABI 1.0 – March 12, 2025 + // + // We don't exactly need these classes for emit since we emit LLVM, but we still need to follow + // the spec and classify everything in order to compute what we need (e.g. sret). + // + // Note: Right now we don't support many of the types that would require some of the classes + // such as SSEUP. For that reason, the classify algorithm is simplified and some shortcuts + // are taken. + + internal enum Class + { + /// + /// This class is used as initializer in the algorithms. It will be used for padding and empty structures and unions. + /// + NO_CLASS, + + /// + /// This class consists of integral types that fit into one of the general purpose registers. + /// + INTEGER, + /// + /// This class consists of types that fit into a vector register. + /// + SSE, + /// + /// This class consists of types that fit into a vector register and can be passed and returned in the upper bytes of it. + /// + SSEUP, + /// + /// This class consists of types that will be returned via the x87 FPU. + /// + X87, + /// + /// This class consists of types that will be returned via the x87 FPU and can be passed and returned in the upper bytes of it. + /// + X87UP, + /// + /// This class consists of types that will be returned via the x87 FPU. + /// + COMPLEX_X87, + /// + /// This class consists of types that will be passed and returned in memory via the stack. + /// + MEMORY, + } + + /// + /// Useful wrapper around 8 classes representing all possible slots as defined by the ABI. + /// Each class corresponds to an eightbyte as defined in 3.2.3. + /// + internal readonly struct Classes + { + // TODO: Optimize + public readonly Class[] _arr = new Class[8]; + + public Classes(params IEnumerable classes) + { + var i = 0; + foreach (var c in classes) + { + if (i > 7) + { + throw new IndexOutOfRangeException("Classes have exactly 8 items."); + } + this[i] = c; + i++; + } + if (i <= 7) + { + // Fill rest with NO_CLASS. + for (var j = i; j < 8; j++) + { + this[j] = Class.NO_CLASS; + } + } + } + + public int Length => _arr.Length; + + public IEnumerable Enumerate() + { + return _arr; + } + + public Class this[int i] + { + get + { + if (i > 8) + { + throw new IndexOutOfRangeException("Classes have exactly 8 slots."); + } + return _arr[i]; + } + set + { + if (i > 8) + { + throw new IndexOutOfRangeException("Classes have exactly 8 slots."); + } + _arr[i] = value; + } + } + + // TODO (self-host): Wrap BitSize into ClassOffset with additional static verification? + + //public Class GetAtOffset(BitSize offset) + //{ + // var i = GetSlotIndexAtOffset(offset); + // return _arr[i]; + //} + + //public void SetAtOffset(BitSize offset, Class c) + //{ + // var i = GetSlotIndexAtOffset(offset); + // _arr[i] = c; + //} + + //private int GetSlotIndexAtOffset(BitSize offset) + //{ + // Debug.Assert(offset.Bytes >= 0 && offset.Bytes <= 8 * 8); // eightbytes * 8 slots + // return offset.Bytes / 8; + //} + } + + private static readonly Classes MemoryClasses = new(Class.MEMORY); + + protected new x86_64LinuxTarget Target => (x86_64LinuxTarget)base.Target; + + internal Classes Classify(HirType type) + { + var layout = Target.GetLayout(type); + var runtimeKind = type.GetRuntimeKind(); + + // "Arguments of types (signed and unsigned) _Bool, char, short, int, long, long long, and pointers are in the INTEGER class." + if (runtimeKind is RuntimeTypeKind.Bool or RuntimeTypeKind.Char or RuntimeTypeKind.Ptr || + type.KnownTypeTag.IsNumericInteger()) + { + return new(Class.INTEGER); + } + + // "Arguments of types _Float16, float, double, _Decimal32, _Decimal64 and __m64 are in class SSE." + if (type.KnownTypeTag.IsNumericFloat()) + { + return new(Class.SSE); + } + + // TODO: "Arguments of types __float128, _Decimal128 and __m128 are split into two halves. The least significant ones belong to class SSE, the most significant one to class SSEUP." + // TODO: "Arguments of type __m256 are split into four eightbyte chunks. The least significant one belongs to class SSE and all the others to class SSEUP." + // TODO: "Arguments of type __m512 are split into eight eightbyte chunks. The least significant one belongs to class SSE and all the others to class SSEUP." + + // TODO: ? "The 64-bit mantissa of arguments of type long double belongs to class X87, the 16 - bit exponent plus 6 bytes of padding belongs to class X87UP." + + // TODO: "Arguments of type __int128 offer the same operations as INTEGERs, yet they do not fit into one general purpose register but require two registers. [...]" + + // TODO: If arbitrary bit sizes become supported: + // - "Arguments of type _BitInt(N) with N <= 64 are in the INTEGER class." + // - "Arguments of type _BitInt(N) with N > 64 are classified as if they were implemented as struct of 64-bit integer fields." + + // TODO: "Arguments of complex T where T is one of the types _Float16, float, double or __float128 are treated as if they are implemented as: [...]" + // TODO: "A variable of type complex long double is classified as type COMPLEX_X87." + + // "The classification of aggregate (structures and arrays) and union types works as follows:" + + // "1. If the size of an object is larger than eight eightbytes, or it contains unaligned fields, it has class MEMORY." + if (layout.Size > 8.AsBytes()) // TODO: or unaligned fields + { + return MemoryClasses; + } + + var aggregateClasses = new Classes(); + + if (runtimeKind is RuntimeTypeKind.Struct) + { + ClassifyStruct(ref aggregateClasses,(HirStructType)type, 0.AsBytes()); + } + + // TODO: Arrays + + // "5. Then a post merger cleanup is done: [...]" + + for (var i = 0; i < aggregateClasses.Length; i++) + { + var c = aggregateClasses[i]; + + // "(a) If one of the classes is MEMORY, the whole argument is passed in memory." + if (c is Class.MEMORY) + return MemoryClasses; + // "(b) If X87UP is not preceded by X87, the whole argument is passed in memory." + if (c is Class.X87UP && (i == 0 || aggregateClasses[i - 1] is not Class.X87)) + return MemoryClasses; + } + + // "(c) If the size of the aggregate exceeds two eightbytes and the first eightbyte + // isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory." + if (layout.Size.Bytes > 2 && + (aggregateClasses[0] is not Class.SSE || !aggregateClasses.Enumerate().Skip(1).All(x => x is Class.SSEUP or Class.NO_CLASS))) + { + return MemoryClasses; + } + + // "(d) If SSEUP is not preceded by SSE or SSEUP, it is converted to SSE." + for (var i = 0; i < aggregateClasses.Length; i++) + { + if (aggregateClasses[i] is Class.SSEUP && aggregateClasses[i - 1] is not Class.SSE and not Class.SSEUP) + { + aggregateClasses[i] = Class.SSE; + } + } + + return aggregateClasses; + } + + internal BitSize ClassifyStruct(ref Classes classes, HirStructType type, BitSize offset) + { + var structLayout = Target.GetStructLayout(type); + var sections = structLayout.Sections; + + // "3. If the size of the aggregate exceeds a single eightbyte, each is classified + // separately. Each eightbyte gets initialized to class NO_CLASS." + // "4. Each field of an object is classified recursively so that always two fields are considered. + + var currentOffset = offset; + foreach (var section in sections) + { + if (section.Kind is StructLayoutSectionKind.Data) + { + var field = type.Fields[section.FieldIndex]; + if (field.Type is HirStructType fieldStructType) + { + currentOffset = ClassifyStruct(ref classes, fieldStructType, currentOffset); + } + else + { + var fieldClasses = Classify(field.Type); + for (int slotIndex = currentOffset.Bytes / 8, j = 0; j < fieldClasses.Length; slotIndex++, j++) + { + Debug.Assert(slotIndex < 8 && slotIndex + j < 8); // eightbytes * 8 slots + + if (fieldClasses[j] is Class.NO_CLASS) + break; + + classes[slotIndex] = MergeInSlot(classes[slotIndex], fieldClasses[j]); + } + } + } + + currentOffset += section.Size; + } + + return currentOffset; + } + + /// + /// Merges two classes occupying the same eightbyte slot. + /// + private Class MergeInSlot(Class a, Class b) + { + // "4. Each field of an object is classified recursively so that always two fields 18 are considered. + // The resulting class is calculated according to the classes of the fields in the eightbyte: [...]" + if (a == b) return a; + if (a == Class.NO_CLASS) return b; + if (b == Class.NO_CLASS) return a; + if (a == Class.MEMORY || b == Class.MEMORY) return Class.MEMORY; + if (a == Class.INTEGER || b == Class.INTEGER) return Class.INTEGER; + if (a == Class.X87 || a == Class.X87UP || a == Class.COMPLEX_X87 || b == Class.X87 || b == Class.X87UP || b == Class.COMPLEX_X87) + return Class.MEMORY; + return Class.SSE; + } + + public override ABIArgInfo ComputeInfo(HirType type, bool isReturnType) + { + var size = Target.GetLayout(type).Size; + if (size.IsZeroSized) + { + return new IgnoreABIArgInfo(); + } + + var classes = Classify(type); + + if (classes[0] is Class.MEMORY) + { + Debug.Assert(classes[1] is Class.NO_CLASS); + return new IndirectABIArgInfo(); + } + + if (classes[0] is Class.SSE && classes[1] is Class.NO_CLASS) + { + return new AsRealABIArgInfo(); + } + + if (classes[0] is Class.INTEGER && classes[1] is Class.NO_CLASS) + { + return new AsIntegerABIArgInfo(); + } + + return new DirectABIArgInfo(); + } +} + +internal sealed class x86_64LinuxGNULLVMTarget(CompilationContext context) : x86_64LinuxLLVMTarget(context) +{ + private new x86_64LinuxGNUTarget Target => (x86_64LinuxGNUTarget)Context.Target; +} From 408d724679e0d5b3d7d80abc8f76fa80bc40f579 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 18:55:50 +0200 Subject: [PATCH 06/14] Set temp target triple when empty to std os related target --- src/compilation/Compiler.cs | 13 +++++++++++-- src/compilation/LLVM/LLVMTarget.cs | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/compilation/Compiler.cs b/src/compilation/Compiler.cs index 6a488e6..efb4266 100644 --- a/src/compilation/Compiler.cs +++ b/src/compilation/Compiler.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Runtime.InteropServices; using Surab.Analysis; using Surab.Compilation.LLVM; @@ -36,8 +37,16 @@ public static string Compile( if (string.IsNullOrEmpty(targetTriple)) { - // temp - targetTriple = "x86_64-pc-windows-msvc"; + // TODO: temp + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + targetTriple = "x86_64-pc-windows-msvc"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + targetTriple = "x86_64-pc-linux-gnu"; + } + Console.WriteLine($"Setting target triple to: {targetTriple}"); } // TODO: std project diags are not being reported right now diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs index 87d1bdb..a4a5fd3 100644 --- a/src/compilation/LLVM/LLVMTarget.cs +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -29,7 +29,7 @@ public static LLVMTarget Create(CompilationContext context) return target switch { x86_64WindowsMSVCTarget => new x86_64WindowsMSVCLLVMTarget(context), - // TODO: linux + x86_64LinuxGNUTarget => new x86_64LinuxGNULLVMTarget(context), _ => throw new UnreachableException(), }; } From 6fdcf9f101cba14c4825e2956348c63108c2dc0e Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 18:58:43 +0200 Subject: [PATCH 07/14] Simplify for now --- src/compilation/LLVM/LLVMTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs index a4a5fd3..924c1e7 100644 --- a/src/compilation/LLVM/LLVMTarget.cs +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -351,13 +351,13 @@ internal enum Class MEMORY, } + // TODO: Optimize /// /// Useful wrapper around 8 classes representing all possible slots as defined by the ABI. /// Each class corresponds to an eightbyte as defined in 3.2.3. /// - internal readonly struct Classes + internal class Classes { - // TODO: Optimize public readonly Class[] _arr = new Class[8]; public Classes(params IEnumerable classes) From 6947aa076aa1c1cad12be4cf0d1a6f5d60322781 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 19:34:59 +0200 Subject: [PATCH 08/14] Avoid CallerFilePath to get repo path Causes problems if for example we try to debug using wsl in VS. --- .gitignore | 1 + src/common/Helpers/PathHelper.cs | 15 ++++++++++++++- src/runner/Program.cs | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5fe29b7..aaba79d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ surab-lsp-server.log *.so *.nupkg editor-groups.json +*.csproj.user diff --git a/src/common/Helpers/PathHelper.cs b/src/common/Helpers/PathHelper.cs index d1e43bd..dfe3c5d 100644 --- a/src/common/Helpers/PathHelper.cs +++ b/src/common/Helpers/PathHelper.cs @@ -6,7 +6,17 @@ internal static class PathHelper { public static string GetRepoRootFolderPath() { - return Path.Combine(GetCallerFilePath(), "../../../../"); + // Climb up until we find it. + var cd = Environment.CurrentDirectory; + // Match when we find surab/src. + while (Path.GetFileName(cd) != "src" && Path.GetFileName(Path.GetDirectoryName(cd)) != "surab") + { + cd = Path.GetDirectoryName(cd); + } + // One more to get repo directory here. + cd = Path.GetDirectoryName(cd); + + return cd!; } public static string GetSrcFolderPath() @@ -29,6 +39,9 @@ public static string GetRuntimeTargetFolderPath() return Path.Combine(GetRepoRootFolderPath(), "target"); } + // Note: Used to use this in GetRepoRootFolderPath but CallerFilePath computes the path on + // compile time so it might produce results not compatible with linux (e.g. if debugging in wsl + // in VS). private static string GetCallerFilePath([CallerFilePath] string callerFilePath = null!) { return callerFilePath; diff --git a/src/runner/Program.cs b/src/runner/Program.cs index af7abef..7213ffb 100644 --- a/src/runner/Program.cs +++ b/src/runner/Program.cs @@ -12,6 +12,7 @@ { FileName = "cargo", Arguments = "build", + WorkingDirectory = runtimeSrcFolderPath, }, }; From f5397323dc2e50af0cc7c8a8bc22fd82ff32ed18 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 19:37:33 +0200 Subject: [PATCH 09/14] Fix message --- src/compilation/LLVM/LinkerRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compilation/LLVM/LinkerRunner.cs b/src/compilation/LLVM/LinkerRunner.cs index 5de4d37..6d6e52f 100644 --- a/src/compilation/LLVM/LinkerRunner.cs +++ b/src/compilation/LLVM/LinkerRunner.cs @@ -50,7 +50,7 @@ [.. objectFiles.Select(objectFile => Path.Combine(wd, objectFile)), surabRuntime outputFilePath, out var error)) { - throw new Exception("Calling lld failed: " + error); + throw new Exception("Calling clang failed: " + error); } var exeFileInfo = new FileInfo(outputFilePath); From 5164f9557d7fb254d3e29d2cb0065ebe3fd5f973 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 19:37:59 +0200 Subject: [PATCH 10/14] Fix computing info --- src/compilation/LLVM/LLVMTarget.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs index 924c1e7..dbfac54 100644 --- a/src/compilation/LLVM/LLVMTarget.cs +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -591,12 +591,14 @@ public override ABIArgInfo ComputeInfo(HirType type, bool isReturnType) return new IndirectABIArgInfo(); } - if (classes[0] is Class.SSE && classes[1] is Class.NO_CLASS) + if (classes[0] is Class.SSE && classes[1] is Class.NO_CLASS && + type.GetRuntimeKind() is RuntimeTypeKind.Struct) { return new AsRealABIArgInfo(); } - if (classes[0] is Class.INTEGER && classes[1] is Class.NO_CLASS) + if (classes[0] is Class.INTEGER && classes[1] is Class.NO_CLASS && + type.GetRuntimeKind() is RuntimeTypeKind.Struct) { return new AsIntegerABIArgInfo(); } From 05ec1957b21174c2047e0712ce1502a3fc4e6eaf Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 20:20:49 +0200 Subject: [PATCH 11/14] Add cargo full path so it works when debugging in WSL --- src/runner/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/runner/Program.cs b/src/runner/Program.cs index 7213ffb..88f46dc 100644 --- a/src/runner/Program.cs +++ b/src/runner/Program.cs @@ -11,6 +11,7 @@ StartInfo = { FileName = "cargo", + //FileName = "/home/mrahhal/.cargo/bin/cargo", // This is the only way this is working when debugging WSL in VS. Not sure why. Arguments = "build", WorkingDirectory = runtimeSrcFolderPath, }, From 4844baff2ab9ffad155d3a6c519ec1365396d075 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 20:33:16 +0200 Subject: [PATCH 12/14] Create launchSettings.json --- src/runner/Properties/launchSettings.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/runner/Properties/launchSettings.json diff --git a/src/runner/Properties/launchSettings.json b/src/runner/Properties/launchSettings.json new file mode 100644 index 0000000..611941d --- /dev/null +++ b/src/runner/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "runner": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "distributionName": "" // Needs a default WSL distro to be linux with the prerequisites installed. + } + } +} From 5bb5220c0257a6eb859127ee32e56f41978e0704 Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 20:34:26 +0200 Subject: [PATCH 13/14] Fix a few bugs during ABI classification --- src/compilation/LLVM/LLVMTarget.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compilation/LLVM/LLVMTarget.cs b/src/compilation/LLVM/LLVMTarget.cs index dbfac54..7c33ca8 100644 --- a/src/compilation/LLVM/LLVMTarget.cs +++ b/src/compilation/LLVM/LLVMTarget.cs @@ -470,7 +470,7 @@ internal Classes Classify(HirType type) // "The classification of aggregate (structures and arrays) and union types works as follows:" // "1. If the size of an object is larger than eight eightbytes, or it contains unaligned fields, it has class MEMORY." - if (layout.Size > 8.AsBytes()) // TODO: or unaligned fields + if (layout.Size > (8 * 8).AsBytes()) // TODO: or unaligned fields { return MemoryClasses; } @@ -500,7 +500,7 @@ internal Classes Classify(HirType type) // "(c) If the size of the aggregate exceeds two eightbytes and the first eightbyte // isn’t SSE or any other eightbyte isn’t SSEUP, the whole argument is passed in memory." - if (layout.Size.Bytes > 2 && + if (layout.Size.Bytes > (2 * 8) && (aggregateClasses[0] is not Class.SSE || !aggregateClasses.Enumerate().Skip(1).All(x => x is Class.SSEUP or Class.NO_CLASS))) { return MemoryClasses; @@ -533,9 +533,9 @@ internal BitSize ClassifyStruct(ref Classes classes, HirStructType type, BitSize if (section.Kind is StructLayoutSectionKind.Data) { var field = type.Fields[section.FieldIndex]; - if (field.Type is HirStructType fieldStructType) + if (field.Type.GetRuntimeKind() is RuntimeTypeKind.Struct) { - currentOffset = ClassifyStruct(ref classes, fieldStructType, currentOffset); + currentOffset = ClassifyStruct(ref classes, (HirStructType)field.Type, currentOffset); } else { From 0a5a715e1d70909de50777f789c44a3406cb6f1e Mon Sep 17 00:00:00 2001 From: Mohammad Rahhal Date: Thu, 29 May 2025 20:36:48 +0200 Subject: [PATCH 14/14] Enable tests in CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2f84b2..84e2e55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,5 +40,5 @@ jobs: - name: Build dotnet run: dotnet build - # - name: Test - # run: dotnet test + - name: Test + run: dotnet test