From 7add5a9139a57bc0743d367aa048f4408c66ff8b Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 25 Feb 2024 22:17:35 +0800 Subject: [PATCH] [Compiler Add] allow multiple smart contract exist in one project (#908) * allow multiple smart contract exist in one project * fix remaining issues * update unit tests * code optimization * Update tests/Neo.SmartContract.TestEngine/TestEngine.cs * Test it: Compile to artifacts * clean changes * fix complication issue * multiple smart contract topology analysis. Making it easier to support cross contract call. * Move artifact generation to the test project * fix conflict * update neo * fix error * this pr apply latest neo to devpack * update signle contract check * add comments --------- Co-authored-by: Shargon --- src/Neo.Compiler.CSharp/CompilationContext.cs | 318 ++++++------------ src/Neo.Compiler.CSharp/CompilationEngine.cs | 249 ++++++++++++++ .../MethodConvert/CallHelpers.cs | 12 +- .../MethodConvert/ConstructorConvert.cs | 8 +- ...AssignmentExpression.CoalesceAssignment.cs | 4 +- .../AssignmentExpression.ComplexAssignment.cs | 4 +- .../AssignmentExpression.SimpleAssignment.cs | 4 +- .../Expression/IdentifierNameExpression.cs | 4 +- .../Expression/MemberExpression.cs | 4 +- .../Expression/ObjectCreationExpression.cs | 2 +- .../UnaryExpression.PostfixUnary.cs | 4 +- .../Expression/UnaryExpression.PrefixUnary.cs | 4 +- .../MethodConvert/ExternConvert.cs | 2 +- .../MethodConvert/MethodConvert.cs | 31 +- .../MethodConvert/PropertyConvert.cs | 4 +- src/Neo.Compiler.CSharp/Program.cs | 14 +- .../Contract_Multiple.cs | 4 - .../UnitTest1.cs | 8 +- ...o.SmartContract.Framework.UnitTests.csproj | 20 +- .../TestEngine.cs | 29 +- 20 files changed, 442 insertions(+), 287 deletions(-) create mode 100644 src/Neo.Compiler.CSharp/CompilationEngine.cs diff --git a/src/Neo.Compiler.CSharp/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationContext.cs index 090a43dd1..17d9ebcb2 100644 --- a/src/Neo.Compiler.CSharp/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationContext.cs @@ -22,14 +22,11 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; -using System.Xml.Linq; using scfx::Neo.SmartContract.Framework.Attributes; using Diagnostic = Microsoft.CodeAnalysis.Diagnostic; @@ -37,76 +34,70 @@ namespace Neo.Compiler { public class CompilationContext { - private static readonly MetadataReference[] commonReferences; - private static readonly Dictionary metaReferences = new(); - private readonly Compilation compilation; - private string? displayName, className; - private bool scTypeFound; - private readonly List diagnostics = new(); - private readonly HashSet supportedStandards = new(); - private readonly List methodsExported = new(); - private readonly List eventsExported = new(); - private readonly PermissionBuilder permissions = new(); - private readonly HashSet trusts = new(); - private readonly JObject manifestExtra = new(); - private readonly MethodConvertCollection methodsConverted = new(); - private readonly MethodConvertCollection methodsForward = new(); - private readonly List methodTokens = new(); - private readonly Dictionary staticFields = new(SymbolEqualityComparer.Default); - private readonly List anonymousStaticFields = new(); - private readonly Dictionary vtables = new(SymbolEqualityComparer.Default); - private byte[]? script; - - public bool Success => !diagnostics.Any(p => p.Severity == DiagnosticSeverity.Error); - public IReadOnlyList Diagnostics => diagnostics; - public string? ContractName => displayName ?? Options.BaseName ?? className; + private readonly CompilationEngine _engine; + readonly INamedTypeSymbol _targetContract; + internal Options Options => _engine.Options; + private string? _displayName, _className; + private readonly List _diagnostics = new(); + private readonly HashSet _supportedStandards = new(); + private readonly List _methodsExported = new(); + private readonly List _eventsExported = new(); + private readonly PermissionBuilder _permissions = new(); + private readonly HashSet _trusts = new(); + private readonly JObject _manifestExtra = new(); + // We can not reuse these converted methods as the offsets are determined while converting + private readonly MethodConvertCollection _methodsConverted = new(); + private readonly MethodConvertCollection _methodsForward = new(); + private readonly List _methodTokens = new(); + private readonly Dictionary _staticFields = new(SymbolEqualityComparer.Default); + private readonly List _anonymousStaticFields = new(); + private readonly Dictionary _vtables = new(SymbolEqualityComparer.Default); + private byte[]? _script; + + public bool Success => _diagnostics.All(p => p.Severity != DiagnosticSeverity.Error); + public IReadOnlyList Diagnostics => _diagnostics; + // TODO: basename should not work when multiple contracts exit in one project + public string? ContractName => _displayName ?? Options.BaseName ?? _className; private string? Source { get; set; } - internal Options Options { get; private set; } - internal IEnumerable StaticFieldSymbols => staticFields.OrderBy(p => p.Value).Select(p => p.Key); - internal IEnumerable<(byte, ITypeSymbol)> VTables => vtables.OrderBy(p => p.Value).Select(p => (p.Value, p.Key)); - internal int StaticFieldCount => staticFields.Count + anonymousStaticFields.Count + vtables.Count; - private byte[] Script => script ??= GetInstructions().Select(p => p.ToArray()).SelectMany(p => p).ToArray(); - static CompilationContext() - { - string coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - commonReferences = new[] - { - MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")), - MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.InteropServices.dll")), - MetadataReference.CreateFromFile(typeof(string).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DisplayNameAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(BigInteger).Assembly.Location) - }; - } + internal IEnumerable StaticFieldSymbols => _staticFields.OrderBy(p => p.Value).Select(p => p.Key); + internal IEnumerable<(byte, ITypeSymbol)> VTables => _vtables.OrderBy(p => p.Value).Select(p => (p.Value, p.Key)); + internal int StaticFieldCount => _staticFields.Count + _anonymousStaticFields.Count + _vtables.Count; + private byte[] Script => _script ??= GetInstructions().Select(p => p.ToArray()).SelectMany(p => p).ToArray(); + - private CompilationContext(Compilation compilation, Options options) + /// + /// Specify the contract to be compiled. + /// + /// CompilationEngine that contains the compilation syntax tree and compiled methods + /// Contract to be compiled + internal CompilationContext(CompilationEngine engine, INamedTypeSymbol targetContract) { - this.compilation = compilation; - this.Options = options; + _engine = engine; + _targetContract = targetContract; } private void RemoveEmptyInitialize() { - int index = methodsExported.FindIndex(p => p.Name == "_initialize"); + int index = _methodsExported.FindIndex(p => p.Name == "_initialize"); if (index < 0) return; - AbiMethod method = methodsExported[index]; - if (methodsConverted[method.Symbol].Instructions.Count <= 1) + AbiMethod method = _methodsExported[index]; + if (_methodsConverted[method.Symbol].Instructions.Count <= 1) { - methodsExported.RemoveAt(index); - methodsConverted.Remove(method.Symbol); + _methodsExported.RemoveAt(index); + _methodsConverted.Remove(method.Symbol); } } private IEnumerable GetInstructions() { - return methodsConverted.SelectMany(p => p.Instructions).Concat(methodsForward.SelectMany(p => p.Instructions)); + return _methodsConverted.SelectMany(p => p.Instructions).Concat(_methodsForward.SelectMany(p => p.Instructions)); } private int GetAbiOffset(IMethodSymbol method) { - if (!methodsForward.TryGetValue(method, out MethodConvert? convert)) - convert = methodsConverted[method]; + if (!_methodsForward.TryGetValue(method, out MethodConvert? convert)) + convert = _methodsConverted[method]; return convert.Instructions[0].Offset; } @@ -118,13 +109,13 @@ private static bool ValidateContractTrust(string value) return false; } - private void Compile() + internal void Compile() { HashSet processed = new(SymbolEqualityComparer.Default); - foreach (SyntaxTree tree in compilation.SyntaxTrees) + foreach (SyntaxTree tree in _engine.Compilation!.SyntaxTrees) { - SemanticModel model = compilation.GetSemanticModel(tree); - diagnostics.AddRange(model.GetDiagnostics().Where(u => u.Severity != DiagnosticSeverity.Hidden)); + SemanticModel model = _engine.Compilation!.GetSemanticModel(tree); + _diagnostics.AddRange(model.GetDiagnostics().Where(u => u.Severity != DiagnosticSeverity.Hidden)); if (!Success) continue; try { @@ -132,16 +123,11 @@ private void Compile() } catch (CompilationException ex) { - diagnostics.Add(ex.Diagnostic); + _diagnostics.Add(ex.Diagnostic); } } if (Success) { - if (!scTypeFound) - { - diagnostics.Add(Diagnostic.Create(DiagnosticId.NoEntryPoint, DiagnosticCategory.Default, "No SmartContract is found in the sources.", DiagnosticSeverity.Error, DiagnosticSeverity.Error, true, 0)); - return; - } RemoveEmptyInitialize(); Instruction[] instructions = GetInstructions().ToArray(); instructions.RebuildOffsets(); @@ -150,110 +136,6 @@ private void Compile() } } - public static CompilationContext Compile(IEnumerable sourceFiles, IEnumerable references, Options options) - { - IEnumerable syntaxTrees = sourceFiles.OrderBy(p => p).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), options: options.GetParseOptions(), path: p)); - if (IsSingleAbstractClass(syntaxTrees)) throw new FormatException("The given class is abstract, no valid neo SmartContract found."); - CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, deterministic: true, nullableContextOptions: options.Nullable); - CSharpCompilation compilation = CSharpCompilation.Create(null, syntaxTrees, references, compilationOptions); - CompilationContext context = new(compilation, options); - context.Compile(); - return context; - } - - public static CompilationContext CompileSources(string[] sourceFiles, Options options) - { - List references = new(commonReferences) - { - MetadataReference.CreateFromFile(typeof(scfx.Neo.SmartContract.Framework.SmartContract).Assembly.Location) - }; - return Compile(sourceFiles, references, options); - } - - public static Compilation GetCompilation(string csproj, Options options) - { - string folder = Path.GetDirectoryName(csproj)!; - string obj = Path.Combine(folder, "obj"); - HashSet sourceFiles = Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories) - .Where(p => !p.StartsWith(obj)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - List references = new(commonReferences); - CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, deterministic: true, nullableContextOptions: options.Nullable); - XDocument document = XDocument.Load(csproj); - sourceFiles.UnionWith(document.Root!.Elements("ItemGroup").Elements("Compile").Attributes("Include").Select(p => Path.GetFullPath(p.Value, folder))); - Process.Start(new ProcessStartInfo - { - FileName = "dotnet", - Arguments = $"restore \"{csproj}\"", - WorkingDirectory = folder - })!.WaitForExit(); - string assetsPath = Path.Combine(folder, "obj", "project.assets.json"); - JObject assets = (JObject)JToken.Parse(File.ReadAllBytes(assetsPath))!; - foreach (var (name, package) in ((JObject)assets["targets"]![0]!).Properties) - { - MetadataReference? reference = GetReference(name, (JObject)package!, assets, folder, options, compilationOptions); - if (reference is not null) references.Add(reference); - } - IEnumerable syntaxTrees = sourceFiles.OrderBy(p => p).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), options: options.GetParseOptions(), path: p)); - return CSharpCompilation.Create(assets["project"]!["restore"]!["projectName"]!.GetString(), syntaxTrees, references, compilationOptions); - } - - private static MetadataReference? GetReference(string name, JObject package, JObject assets, string folder, Options options, CSharpCompilationOptions compilationOptions) - { - string assemblyName = Path.GetDirectoryName(name)!; - if (!metaReferences.TryGetValue(assemblyName, out var reference)) - { - switch (assets["libraries"]![name]!["type"]!.GetString()) - { - case "package": - string packagesPath = assets["project"]!["restore"]!["packagesPath"]!.GetString(); - string namePath = assets["libraries"]![name]!["path"]!.GetString(); - string[] files = ((JArray)assets["libraries"]![name]!["files"]!) - .Select(p => p!.GetString()) - .Where(p => p.StartsWith("src/")) - .ToArray(); - if (files.Length == 0) - { - JObject? dllFiles = (JObject?)(package["compile"] ?? package["runtime"]); - if (dllFiles is null) return null; - foreach (var (file, _) in dllFiles.Properties) - { - if (file.EndsWith("_._")) continue; - string path = Path.Combine(packagesPath, namePath, file); - if (!File.Exists(path)) continue; - reference = MetadataReference.CreateFromFile(path); - break; - } - if (reference is null) return null; - } - else - { - IEnumerable st = files.OrderBy(p => p).Select(p => Path.Combine(packagesPath, namePath, p)).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), path: p)); - CSharpCompilation cr = CSharpCompilation.Create(assemblyName, st, commonReferences, compilationOptions); - reference = cr.ToMetadataReference(); - } - break; - case "project": - string msbuildProject = assets["libraries"]![name]!["msbuildProject"]!.GetString(); - msbuildProject = Path.GetFullPath(msbuildProject, folder); - reference = GetCompilation(msbuildProject, options).ToMetadataReference(); - break; - default: - throw new NotSupportedException(); - } - metaReferences.Add(assemblyName, reference); - } - return reference; - } - - public static CompilationContext CompileProject(string csproj, Options options) - { - Compilation compilation = GetCompilation(csproj, options); - CompilationContext context = new(compilation, options); - context.Compile(); - return context; - } - public NefFile CreateExecutable() { Assembly assembly = Assembly.GetExecutingAssembly(); @@ -263,7 +145,7 @@ public NefFile CreateExecutable() { Compiler = $"{titleAttribute.Title} {versionAttribute.InformationalVersion}", Source = Source ?? string.Empty, - Tokens = methodTokens.ToArray(), + Tokens = _methodTokens.ToArray(), Script = Script }; @@ -292,14 +174,14 @@ static void WriteMethod(StringBuilder builder, MethodConvert method) builder.AppendLine(); } StringBuilder builder = new(); - foreach (MethodConvert method in methodsConverted) + foreach (MethodConvert method in _methodsConverted) { builder.Append("// "); builder.AppendLine(method.Symbol.ToString()); builder.AppendLine(); WriteMethod(builder, method); } - foreach (MethodConvert method in methodsForward) + foreach (MethodConvert method in _methodsForward) { builder.Append("// "); builder.Append(method.Symbol.ToString()); @@ -317,10 +199,10 @@ public ContractManifest CreateManifest() ["name"] = ContractName, ["groups"] = new JArray(), ["features"] = new JObject(), - ["supportedstandards"] = supportedStandards.OrderBy(p => p).Select(p => (JString)p!).ToArray(), + ["supportedstandards"] = _supportedStandards.OrderBy(p => p).Select(p => (JString)p!).ToArray(), ["abi"] = new JObject { - ["methods"] = methodsExported.Select(p => new JObject + ["methods"] = _methodsExported.Select(p => new JObject { ["name"] = p.Name, ["offset"] = GetAbiOffset(p.Symbol), @@ -328,15 +210,15 @@ public ContractManifest CreateManifest() ["returntype"] = p.ReturnType, ["parameters"] = p.Parameters.Select(p => p.ToJson()).ToArray() }).ToArray(), - ["events"] = eventsExported.Select(p => new JObject + ["events"] = _eventsExported.Select(p => new JObject { ["name"] = p.Name, ["parameters"] = p.Parameters.Select(p => p.ToJson()).ToArray() }).ToArray() }, - ["permissions"] = permissions.ToJson(), - ["trusts"] = trusts.Contains("*") ? "*" : trusts.OrderBy(p => p.Length).ThenBy(p => p).Select(u => new JString(u)).ToArray(), - ["extra"] = manifestExtra + ["permissions"] = _permissions.ToJson(), + ["trusts"] = _trusts.Contains("*") ? "*" : _trusts.OrderBy(p => p.Length).ThenBy(p => p).Select(u => new JString(u)).ToArray(), + ["extra"] = _manifestExtra }; // Ensure that is serializable @@ -347,7 +229,7 @@ public JObject CreateDebugInformation(string folder = "") { List documents = new(); List methods = new(); - foreach (var m in methodsConverted.Where(p => p.SyntaxNode is not null)) + foreach (var m in _methodsConverted.Where(p => p.SyntaxNode is not null)) { List sequencePoints = new(); foreach (var ins in m.Instructions.Where(i => i.SourceLocation?.SourceTree is not null)) @@ -392,9 +274,9 @@ public JObject CreateDebugInformation(string folder = "") ["hash"] = Script.ToScriptHash().ToString(), ["documents"] = documents.Select(p => (JString)p!).ToArray(), ["document-root"] = string.IsNullOrEmpty(folder) ? JToken.Null : folder, - ["static-variables"] = staticFields.OrderBy(p => p.Value).Select(p => ((JString)$"{p.Key.Name},{p.Key.Type.GetContractParameterType()},{p.Value}")!).ToArray(), + ["static-variables"] = _staticFields.OrderBy(p => p.Value).Select(p => ((JString)$"{p.Key.Name},{p.Key.Type.GetContractParameterType()},{p.Value}")!).ToArray(), ["methods"] = methods.ToArray(), - ["events"] = eventsExported.Select(e => new JObject + ["events"] = _eventsExported.Select(e => new JObject { ["id"] = e.Name, ["name"] = $"{e.Symbol.ContainingType},{e.Symbol.Name}", @@ -431,40 +313,52 @@ private void ProcessClass(SemanticModel model, INamedTypeSymbol symbol) bool isAbstract = symbol.IsAbstract; bool isContractType = symbol.IsSubclassOf(nameof(scfx.Neo.SmartContract.Framework.SmartContract)); bool isSmartContract = isPublic && !isAbstract && isContractType; + if (isSmartContract) { - if (scTypeFound) throw new CompilationException(DiagnosticId.MultiplyContracts, "Only one smart contract is allowed."); - scTypeFound = true; + // Considering that the complication will process all classes for every smart contract + // it is possible to process multiple smart contract classes in the same project + // As a result, we must stop the process if the current contract class is not the target contract + // For example, if the target contract is "Contract1" and the project contains "Contract1" and "Contract2" + // the process must skip when the "Contract2" class is processed + if (_targetContract.Name != symbol.Name) + { + return; + } + // Process the target contract + // add this compilation context + _engine.Contexts.Add(symbol, this); + foreach (var attribute in symbol.GetAttributesWithInherited()) { if (attribute.AttributeClass!.IsSubclassOf(nameof(ManifestExtraAttribute))) { - manifestExtra[ManifestExtraAttribute.AttributeType[attribute.AttributeClass!.Name]] = (string)attribute.ConstructorArguments[0].Value!; + _manifestExtra[ManifestExtraAttribute.AttributeType[attribute.AttributeClass!.Name]] = (string)attribute.ConstructorArguments[0].Value!; continue; } switch (attribute.AttributeClass!.Name) { case nameof(DisplayNameAttribute): - displayName = (string)attribute.ConstructorArguments[0].Value!; + _displayName = (string)attribute.ConstructorArguments[0].Value!; break; case nameof(ContractSourceCodeAttribute): Source = (string)attribute.ConstructorArguments[0].Value!; break; case nameof(ManifestExtraAttribute): - manifestExtra[(string)attribute.ConstructorArguments[0].Value!] = (string)attribute.ConstructorArguments[1].Value!; + _manifestExtra[(string)attribute.ConstructorArguments[0].Value!] = (string)attribute.ConstructorArguments[1].Value!; break; case nameof(ContractPermissionAttribute): - permissions.Add((string)attribute.ConstructorArguments[0].Value!, attribute.ConstructorArguments[1].Values.Select(p => (string)p.Value!).ToArray()); + _permissions.Add((string)attribute.ConstructorArguments[0].Value!, attribute.ConstructorArguments[1].Values.Select(p => (string)p.Value!).ToArray()); break; case nameof(ContractTrustAttribute): string trust = (string)attribute.ConstructorArguments[0].Value!; if (!ValidateContractTrust(trust)) throw new ArgumentException($"The value {trust} is not a valid one for ContractTrust"); - trusts.Add(trust); + _trusts.Add(trust); break; case nameof(SupportedStandardsAttribute): - supportedStandards.UnionWith( + _supportedStandards.UnionWith( attribute.ConstructorArguments[0].Values .Select(p => p.Value) .Select(p => @@ -477,7 +371,7 @@ private void ProcessClass(SemanticModel model, INamedTypeSymbol symbol) break; } } - className = symbol.Name; + _className = symbol.Name; } foreach (ISymbol member in symbol.GetAllMembers()) { @@ -493,10 +387,10 @@ private void ProcessClass(SemanticModel model, INamedTypeSymbol symbol) } if (isSmartContract) { - IMethodSymbol _initialize = symbol.StaticConstructors.Length == 0 + IMethodSymbol initialize = symbol.StaticConstructors.Length == 0 ? symbol.GetAllMembers().OfType().First(p => p.Name == "_initialize") : symbol.StaticConstructors[0]; - ProcessMethod(model, _initialize, true); + ProcessMethod(model, initialize, true); } } @@ -511,12 +405,12 @@ private void ProcessEvent(IEventSymbol symbol) internal void AddEvent(AbiEvent ev, bool throwErrorIfExists) { - if (eventsExported.Any(u => u.Name == ev.Name)) + if (_eventsExported.Any(u => u.Name == ev.Name)) { if (!throwErrorIfExists) return; throw new CompilationException(ev.Symbol, DiagnosticId.EventNameConflict, $"Duplicate event name: {ev.Name}."); } - eventsExported.Add(ev); + _eventsExported.Add(ev); } private void ProcessMethod(SemanticModel model, IMethodSymbol symbol, bool export) @@ -532,9 +426,9 @@ private void ProcessMethod(SemanticModel model, IMethodSymbol symbol, bool expor if (export) { AbiMethod method = new(symbol); - if (methodsExported.Any(u => u.Name == method.Name && u.Parameters.Length == method.Parameters.Length)) + if (_methodsExported.Any(u => u.Name == method.Name && u.Parameters.Length == method.Parameters.Length)) throw new CompilationException(symbol, DiagnosticId.MethodNameConflict, $"Duplicate method key: {method.Name},{method.Parameters.Length}."); - methodsExported.Add(method); + _methodsExported.Add(method); } if (symbol.GetAttributesWithInherited() @@ -552,16 +446,16 @@ private void ProcessMethod(SemanticModel model, IMethodSymbol symbol, bool expor { MethodConvert forward = new(this, symbol); forward.ConvertForward(model, convert); - methodsForward.Add(forward); + _methodsForward.Add(forward); } } internal MethodConvert ConvertMethod(SemanticModel model, IMethodSymbol symbol) { - if (!methodsConverted.TryGetValue(symbol, out MethodConvert? method)) + if (!_methodsConverted.TryGetValue(symbol, out MethodConvert? method)) { method = new MethodConvert(this, symbol); - methodsConverted.Add(method); + _methodsConverted.Add(method); if (!symbol.DeclaringSyntaxReferences.IsEmpty) { ISourceAssemblySymbol assembly = (ISourceAssemblySymbol)symbol.ContainingAssembly; @@ -574,9 +468,9 @@ internal MethodConvert ConvertMethod(SemanticModel model, IMethodSymbol symbol) internal ushort AddMethodToken(UInt160 hash, string method, ushort parametersCount, bool hasReturnValue, CallFlags callFlags) { - int index = methodTokens.FindIndex(p => p.Hash == hash && p.Method == method && p.ParametersCount == parametersCount && p.HasReturnValue == hasReturnValue && p.CallFlags == callFlags); + int index = _methodTokens.FindIndex(p => p.Hash == hash && p.Method == method && p.ParametersCount == parametersCount && p.HasReturnValue == hasReturnValue && p.CallFlags == callFlags); if (index >= 0) return (ushort)index; - methodTokens.Add(new MethodToken + _methodTokens.Add(new MethodToken { Hash = hash, Method = method, @@ -584,16 +478,16 @@ internal ushort AddMethodToken(UInt160 hash, string method, ushort parametersCou HasReturnValue = hasReturnValue, CallFlags = callFlags }); - permissions.Add(hash.ToString(), method); - return (ushort)(methodTokens.Count - 1); + _permissions.Add(hash.ToString(), method); + return (ushort)(_methodTokens.Count - 1); } internal byte AddStaticField(IFieldSymbol symbol) { - if (!staticFields.TryGetValue(symbol, out byte index)) + if (!_staticFields.TryGetValue(symbol, out byte index)) { index = (byte)StaticFieldCount; - staticFields.Add(symbol, index); + _staticFields.Add(symbol, index); } return index; } @@ -601,28 +495,18 @@ internal byte AddStaticField(IFieldSymbol symbol) internal byte AddAnonymousStaticField() { byte index = (byte)StaticFieldCount; - anonymousStaticFields.Add(index); + _anonymousStaticFields.Add(index); return index; } internal byte AddVTable(ITypeSymbol type) { - if (!vtables.TryGetValue(type, out byte index)) + if (!_vtables.TryGetValue(type, out byte index)) { index = (byte)StaticFieldCount; - vtables.Add(type, index); + _vtables.Add(type, index); } return index; } - - private static bool IsSingleAbstractClass(IEnumerable syntaxTrees) - { - if (syntaxTrees.Count() != 1) return false; - - var tree = syntaxTrees.First(); - var classDeclarations = tree.GetCompilationUnitRoot().DescendantNodes().OfType().ToList(); - - return classDeclarations.Count == 1 && classDeclarations[0].Modifiers.Any(SyntaxKind.AbstractKeyword); - } } } diff --git a/src/Neo.Compiler.CSharp/CompilationEngine.cs b/src/Neo.Compiler.CSharp/CompilationEngine.cs new file mode 100644 index 000000000..9d46344a6 --- /dev/null +++ b/src/Neo.Compiler.CSharp/CompilationEngine.cs @@ -0,0 +1,249 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +extern alias scfx; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Akka.Util.Internal; +using BigInteger = System.Numerics.BigInteger; + +namespace Neo.Compiler +{ + public class CompilationEngine + { + internal Compilation? Compilation; + internal Options Options { get; private set; } + private static readonly MetadataReference[] CommonReferences; + private static readonly Dictionary MetaReferences = new(); + internal readonly Dictionary Contexts = new(SymbolEqualityComparer.Default); + + static CompilationEngine() + { + string coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + CommonReferences = new MetadataReference[] + { + MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")), + MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.InteropServices.dll")), + MetadataReference.CreateFromFile(typeof(string).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DisplayNameAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(BigInteger).Assembly.Location) + }; + } + + public CompilationEngine(Options options) + { + Options = options; + } + + public List Compile(IEnumerable sourceFiles, IEnumerable references) + { + IEnumerable syntaxTrees = sourceFiles.OrderBy(p => p).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), options: Options.GetParseOptions(), path: p)); + CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, deterministic: true, nullableContextOptions: Options.Nullable); + Compilation = CSharpCompilation.Create(null, syntaxTrees, references, compilationOptions); + return CompileProjectContracts(Compilation); + } + + public List CompileSources(string[] sourceFiles) + { + List references = new(CommonReferences) + { + MetadataReference.CreateFromFile(typeof(scfx.Neo.SmartContract.Framework.SmartContract).Assembly.Location) + }; + return Compile(sourceFiles, references); + } + + public List CompileProject(string csproj) + { + Compilation = GetCompilation(csproj); + return CompileProjectContracts(Compilation); + } + + private List CompileProjectContracts(Compilation compilation) + { + var classDependencies = new Dictionary>(SymbolEqualityComparer.Default); + var allSmartContracts = new HashSet(SymbolEqualityComparer.Default); + + foreach (var tree in compilation.SyntaxTrees) + { + var semanticModel = compilation.GetSemanticModel(tree); + var classNodes = tree.GetRoot().DescendantNodes().OfType(); + + foreach (var classNode in classNodes) + { + var classSymbol = semanticModel.GetDeclaredSymbol(classNode); + if (classSymbol != null && IsDerivedFromSmartContract(classSymbol, "Neo.SmartContract.Framework.SmartContract", semanticModel)) + { + allSmartContracts.Add(classSymbol); + classDependencies[classSymbol] = new List(); + foreach (var member in classSymbol.GetMembers()) + { + var memberTypeSymbol = (member as IFieldSymbol)?.Type ?? (member as IPropertySymbol)?.Type; + if (memberTypeSymbol is INamedTypeSymbol namedTypeSymbol && allSmartContracts.Contains(namedTypeSymbol)) + { + classDependencies[classSymbol].Add(namedTypeSymbol); + } + } + } + } + } + + // Verify if there is any valid smart contract class + if (classDependencies.Count == 0) throw new FormatException("No valid neo SmartContract found. Please make sure your contract is subclass of SmartContract and is not abstract."); + // Check contract dependencies, make sure there is no cycle in the dependency graph + var sortedClasses = TopologicalSort(classDependencies); + foreach (var classSymbol in sortedClasses) + { + new CompilationContext(this, classSymbol).Compile(); + } + + return Contexts.Select(p => p.Value).ToList(); + } + + private static List TopologicalSort(Dictionary> dependencies) + { + var sorted = new List(); + var visited = new HashSet(SymbolEqualityComparer.Default); + var visiting = new HashSet(SymbolEqualityComparer.Default); // 添加中间状态以检测循环依赖 + + void Visit(INamedTypeSymbol classSymbol) + { + if (visited.Contains(classSymbol)) + { + return; + } + if (!visiting.Add(classSymbol)) + { + throw new InvalidOperationException("Cyclic dependency detected"); + } + + if (dependencies.TryGetValue(classSymbol, out var dependency)) + { + foreach (var dep in dependency) + { + Visit(dep); + } + } + + visiting.Remove(classSymbol); + visited.Add(classSymbol); + sorted.Add(classSymbol); + } + + foreach (var classSymbol in dependencies.Keys) + { + Visit(classSymbol); + } + + return sorted; + } + + static bool IsDerivedFromSmartContract(INamedTypeSymbol classSymbol, string smartContractFullyQualifiedName, SemanticModel semanticModel) + { + var baseType = classSymbol.BaseType; + while (baseType != null) + { + if (baseType.ToDisplayString() == smartContractFullyQualifiedName) + { + return true; + } + baseType = baseType.BaseType; + } + return false; + } + + public Compilation GetCompilation(string csproj) + { + string folder = Path.GetDirectoryName(csproj)!; + string obj = Path.Combine(folder, "obj"); + HashSet sourceFiles = Directory.EnumerateFiles(folder, "*.cs", SearchOption.AllDirectories) + .Where(p => !p.StartsWith(obj)) + .GroupBy(Path.GetFileName) + .Select(g => g.First()) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + List references = new(CommonReferences); + CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary, deterministic: true, nullableContextOptions: Options.Nullable); + XDocument document = XDocument.Load(csproj); + sourceFiles.UnionWith(document.Root!.Elements("ItemGroup").Elements("Compile").Attributes("Include").Select(p => Path.GetFullPath(p.Value, folder))); + Process.Start(new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"restore \"{csproj}\"", + WorkingDirectory = folder + })!.WaitForExit(); + string assetsPath = Path.Combine(folder, "obj", "project.assets.json"); + JObject assets = (JObject)JToken.Parse(File.ReadAllBytes(assetsPath))!; + foreach (var (name, package) in ((JObject)assets["targets"]![0]!).Properties) + { + MetadataReference? reference = GetReference(name, (JObject)package!, assets, folder, Options, compilationOptions); + if (reference is not null) references.Add(reference); + } + IEnumerable syntaxTrees = sourceFiles.OrderBy(p => p).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), options: Options.GetParseOptions(), path: p)); + return CSharpCompilation.Create(assets["project"]!["restore"]!["projectName"]!.GetString(), syntaxTrees, references, compilationOptions); + } + + private MetadataReference? GetReference(string name, JObject package, JObject assets, string folder, Options options, CSharpCompilationOptions compilationOptions) + { + string assemblyName = Path.GetDirectoryName(name)!; + if (!MetaReferences.TryGetValue(assemblyName, out var reference)) + { + switch (assets["libraries"]![name]!["type"]!.GetString()) + { + case "package": + string packagesPath = assets["project"]!["restore"]!["packagesPath"]!.GetString(); + string namePath = assets["libraries"]![name]!["path"]!.GetString(); + string[] files = ((JArray)assets["libraries"]![name]!["files"]!) + .Select(p => p!.GetString()) + .Where(p => p.StartsWith("src/")) + .ToArray(); + if (files.Length == 0) + { + JObject? dllFiles = (JObject?)(package["compile"] ?? package["runtime"]); + if (dllFiles is null) return null; + foreach (var (file, _) in dllFiles.Properties) + { + if (file.EndsWith("_._")) continue; + string path = Path.Combine(packagesPath, namePath, file); + if (!File.Exists(path)) continue; + reference = MetadataReference.CreateFromFile(path); + break; + } + if (reference is null) return null; + } + else + { + IEnumerable st = files.OrderBy(p => p).Select(p => Path.Combine(packagesPath, namePath, p)).Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p), path: p)); + CSharpCompilation cr = CSharpCompilation.Create(assemblyName, st, CommonReferences, compilationOptions); + reference = cr.ToMetadataReference(); + } + break; + case "project": + string msbuildProject = assets["libraries"]![name]!["msbuildProject"]!.GetString(); + msbuildProject = Path.GetFullPath(msbuildProject, folder); + reference = GetCompilation(msbuildProject).ToMetadataReference(); + break; + default: + throw new NotSupportedException(); + } + MetaReferences.Add(assemblyName, reference); + } + return reference; + } + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/CallHelpers.cs b/src/Neo.Compiler.CSharp/MethodConvert/CallHelpers.cs index b10929e9c..84d3fab3a 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/CallHelpers.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/CallHelpers.cs @@ -35,7 +35,7 @@ private Instruction Call(InteropDescriptor descriptor) private Instruction Call(UInt160 hash, string method, ushort parametersCount, bool hasReturnValue, CallFlags callFlags = CallFlags.All) { - ushort token = context.AddMethodToken(hash, method, parametersCount, hasReturnValue, callFlags); + ushort token = _context.AddMethodToken(hash, method, parametersCount, hasReturnValue, callFlags); return AddInstruction(new Instruction { OpCode = OpCode.CALLT, @@ -58,7 +58,7 @@ private void Call(SemanticModel model, IMethodSymbol symbol, bool instanceOnStac } else { - convert = context.ConvertMethod(model, symbol); + convert = _context.ConvertMethod(model, symbol); methodCallingConvention = convert._callingConvention; } bool isConstructor = symbol.MethodKind == MethodKind.Constructor; @@ -103,8 +103,8 @@ private void Call(SemanticModel model, IMethodSymbol symbol, ExpressionSyntax? i else { convert = symbol.ReducedFrom is null - ? context.ConvertMethod(model, symbol) - : context.ConvertMethod(model, symbol.ReducedFrom); + ? _context.ConvertMethod(model, symbol) + : _context.ConvertMethod(model, symbol.ReducedFrom); methodCallingConvention = convert._callingConvention; } if (!symbol.IsStatic && methodCallingConvention != CallingConvention.Cdecl) @@ -143,7 +143,7 @@ private void Call(SemanticModel model, IMethodSymbol symbol, CallingConvention c } else { - convert = context.ConvertMethod(model, symbol); + convert = _context.ConvertMethod(model, symbol); methodCallingConvention = convert._callingConvention; } int pc = symbol.Parameters.Length; @@ -175,7 +175,7 @@ private void Call(SemanticModel model, IMethodSymbol symbol, CallingConvention c private void EmitCall(MethodConvert target) { - if (target._inline && !context.Options.NoInline) + if (target._inline && !_context.Options.NoInline) for (int i = 0; i < target._instructions.Count - 1; i++) AddInstruction(target._instructions[i].Clone()); else diff --git a/src/Neo.Compiler.CSharp/MethodConvert/ConstructorConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/ConstructorConvert.cs index eb3f9e81a..73cec90c9 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/ConstructorConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/ConstructorConvert.cs @@ -61,19 +61,19 @@ private void ProcessConstructorInitializer(SemanticModel model) private void ProcessStaticFields(SemanticModel model) { - foreach (INamedTypeSymbol @class in context.StaticFieldSymbols.Select(p => p.ContainingType).Distinct(SymbolEqualityComparer.Default).ToArray()) + foreach (INamedTypeSymbol @class in _context.StaticFieldSymbols.Select(p => p.ContainingType).Distinct(SymbolEqualityComparer.Default).ToArray()) { foreach (IFieldSymbol field in @class.GetAllMembers().OfType()) { if (field.IsConst || !field.IsStatic) continue; ProcessFieldInitializer(model, field, null, () => { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.STSFLD, index); }); } } - foreach (var (fieldIndex, type) in context.VTables) + foreach (var (fieldIndex, type) in _context.VTables) { IMethodSymbol[] virtualMethods = type.GetAllMembers().OfType().Where(p => p.IsVirtualMethod()).ToArray(); for (int i = virtualMethods.Length - 1; i >= 0; i--) @@ -85,7 +85,7 @@ private void ProcessStaticFields(SemanticModel model) } else { - MethodConvert convert = context.ConvertMethod(model, method); + MethodConvert convert = _context.ConvertMethod(model, method); Jump(OpCode.PUSHA, convert._startTarget); } } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs index 5ff26de6c..4d9cccf14 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs @@ -129,7 +129,7 @@ private void ConvertFieldIdentifierNameCoalesceAssignment(SemanticModel model, I JumpTarget endTarget = new(); if (left.IsStatic) { - byte index = context.AddStaticField(left); + byte index = _context.AddStaticField(left); AccessSlot(OpCode.LDSFLD, index); AddInstruction(OpCode.ISNULL); Jump(OpCode.JMPIF_L, assignmentTarget); @@ -232,7 +232,7 @@ private void ConvertFieldMemberAccessCoalesceAssignment(SemanticModel model, Mem JumpTarget endTarget = new(); if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.LDSFLD, index); AddInstruction(OpCode.ISNULL); Jump(OpCode.JMPIF_L, assignmentTarget); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs index 626d906d2..c1553010b 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs @@ -115,7 +115,7 @@ private void ConvertFieldIdentifierNameComplexAssignment(SemanticModel model, IT { if (left.IsStatic) { - byte index = context.AddStaticField(left); + byte index = _context.AddStaticField(left); AccessSlot(OpCode.LDSFLD, index); ConvertExpression(model, right); EmitComplexAssignmentOperator(type, operatorToken); @@ -184,7 +184,7 @@ private void ConvertFieldMemberAccessComplexAssignment(SemanticModel model, ITyp { if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.LDSFLD, index); ConvertExpression(model, right); EmitComplexAssignmentOperator(type, operatorToken); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs index 7e0fd822a..fa1723346 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs @@ -104,7 +104,7 @@ private void ConvertIdentifierNameAssignment(SemanticModel model, IdentifierName case IFieldSymbol field: if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.STSFLD, index); } else @@ -139,7 +139,7 @@ private void ConvertMemberAccessAssignment(SemanticModel model, MemberAccessExpr case IFieldSymbol field: if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.STSFLD, index); } else diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/IdentifierNameExpression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/IdentifierNameExpression.cs index 75cb3b063..049439c0c 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/IdentifierNameExpression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/IdentifierNameExpression.cs @@ -32,7 +32,7 @@ private void ConvertIdentifierNameExpression(SemanticModel model, IdentifierName } else if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.LDSFLD, index); } else @@ -52,7 +52,7 @@ private void ConvertIdentifierNameExpression(SemanticModel model, IdentifierName case IMethodSymbol method: if (!method.IsStatic) throw new CompilationException(expression, DiagnosticId.NonStaticDelegate, $"Unsupported delegate: {method}"); - MethodConvert convert = context.ConvertMethod(model, method); + MethodConvert convert = _context.ConvertMethod(model, method); Jump(OpCode.PUSHA, convert._startTarget); break; case IParameterSymbol parameter: diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/MemberExpression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/MemberExpression.cs index 44b488ab0..425d58ece 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/MemberExpression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/MemberExpression.cs @@ -32,7 +32,7 @@ private void ConvertMemberAccessExpression(SemanticModel model, MemberAccessExpr } else if (field.IsStatic) { - byte index = context.AddStaticField(field); + byte index = _context.AddStaticField(field); AccessSlot(OpCode.LDSFLD, index); } else @@ -46,7 +46,7 @@ private void ConvertMemberAccessExpression(SemanticModel model, MemberAccessExpr case IMethodSymbol method: if (!method.IsStatic) throw new CompilationException(expression, DiagnosticId.NonStaticDelegate, $"Unsupported delegate: {method}"); - MethodConvert convert = context.ConvertMethod(model, method); + MethodConvert convert = _context.ConvertMethod(model, method); Jump(OpCode.PUSHA, convert._startTarget); break; case IPropertySymbol property: diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/ObjectCreationExpression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/ObjectCreationExpression.cs index 9f4eed8f3..3b083c83f 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/ObjectCreationExpression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/ObjectCreationExpression.cs @@ -80,7 +80,7 @@ private void ConvertDelegateCreationExpression(SemanticModel model, BaseObjectCr IMethodSymbol symbol = (IMethodSymbol)model.GetSymbolInfo(expression.ArgumentList.Arguments[0].Expression).Symbol!; if (!symbol.IsStatic) throw new CompilationException(expression, DiagnosticId.NonStaticDelegate, $"Unsupported delegate: {symbol}"); - MethodConvert convert = context.ConvertMethod(model, symbol); + MethodConvert convert = _context.ConvertMethod(model, symbol); Jump(OpCode.PUSHA, convert._startTarget); } } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs index f1826db21..0039ec6f9 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs @@ -113,7 +113,7 @@ private void ConvertFieldIdentifierNamePostIncrementOrDecrementExpression(Syntax { if (symbol.IsStatic) { - byte index = context.AddStaticField(symbol); + byte index = _context.AddStaticField(symbol); AccessSlot(OpCode.LDSFLD, index); AddInstruction(OpCode.DUP); EmitIncrementOrDecrement(operatorToken, symbol.Type); @@ -192,7 +192,7 @@ private void ConvertFieldMemberAccessPostIncrementOrDecrementExpression(Semantic { if (symbol.IsStatic) { - byte index = context.AddStaticField(symbol); + byte index = _context.AddStaticField(symbol); AccessSlot(OpCode.LDSFLD, index); AddInstruction(OpCode.DUP); EmitIncrementOrDecrement(operatorToken, symbol.Type); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs index c22b766a3..9535634a6 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs @@ -130,7 +130,7 @@ private void ConvertFieldIdentifierNamePreIncrementOrDecrementExpression(SyntaxT { if (symbol.IsStatic) { - byte index = context.AddStaticField(symbol); + byte index = _context.AddStaticField(symbol); AccessSlot(OpCode.LDSFLD, index); EmitIncrementOrDecrement(operatorToken, symbol.Type); AddInstruction(OpCode.DUP); @@ -209,7 +209,7 @@ private void ConvertFieldMemberAccessPreIncrementOrDecrementExpression(SemanticM { if (symbol.IsStatic) { - byte index = context.AddStaticField(symbol); + byte index = _context.AddStaticField(symbol); AccessSlot(OpCode.LDSFLD, index); EmitIncrementOrDecrement(operatorToken, symbol.Type); AddInstruction(OpCode.DUP); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/ExternConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/ExternConvert.cs index df60e6d81..5cd780a21 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/ExternConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/ExternConvert.cs @@ -73,7 +73,7 @@ private void ConvertExtern() } else if (Symbol.ToString()?.Equals("Neo.SmartContract.Framework.Services.Runtime.Debug(string)") == true) { - context.AddEvent(new AbiEvent(Symbol, "Debug", new SmartContract.Manifest.ContractParameterDefinition() { Name = "message", Type = ContractParameterType.String }), false); + _context.AddEvent(new AbiEvent(Symbol, "Debug", new SmartContract.Manifest.ContractParameterDefinition() { Name = "message", Type = ContractParameterType.String }), false); } if (!emitted) throw new CompilationException(Symbol, DiagnosticId.ExternMethod, $"Unknown method: {Symbol}"); } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs index 21fb5e141..c67fb05d9 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs @@ -32,7 +32,7 @@ partial class MethodConvert { #region Fields - private readonly CompilationContext context; + private readonly CompilationContext _context; private CallingConvention _callingConvention = CallingConvention.Cdecl; private bool _inline; private bool _internalInline; @@ -73,7 +73,7 @@ partial class MethodConvert public MethodConvert(CompilationContext context, IMethodSymbol symbol) { this.Symbol = symbol; - this.context = context; + this._context = context; this._checkedStack.Push(context.Options.Checked); } @@ -103,13 +103,13 @@ private byte AddAnonymousVariable() private void RemoveAnonymousVariable(byte index) { - if (!context.Options.NoOptimize) + if (!_context.Options.NoOptimize) _anonymousVariables.Remove(index); } private void RemoveLocalVariable(ILocalSymbol symbol) { - if (!context.Options.NoOptimize) + if (!_context.Options.NoOptimize) _localVariables.Remove(symbol); } @@ -144,12 +144,12 @@ public void Convert(SemanticModel model) if (Symbol.Name == "_initialize") { ProcessStaticFields(model); - if (context.StaticFieldCount > 0) + if (_context.StaticFieldCount > 0) { _instructions.Insert(0, new Instruction { OpCode = OpCode.INITSSLOT, - Operand = new[] { (byte)context.StaticFieldCount } + Operand = new[] { (byte)_context.StaticFieldCount } }); } } @@ -178,12 +178,12 @@ public void Convert(SemanticModel model) } var modifiers = ConvertModifier(model).ToArray(); ConvertSource(model); - if (Symbol.MethodKind == MethodKind.StaticConstructor && context.StaticFieldCount > 0) + if (Symbol.MethodKind == MethodKind.StaticConstructor && _context.StaticFieldCount > 0) { _instructions.Insert(0, new Instruction { OpCode = OpCode.INITSSLOT, - Operand = new[] { (byte)context.StaticFieldCount } + Operand = new[] { (byte)_context.StaticFieldCount } }); } if (_initslot) @@ -226,7 +226,7 @@ public void Convert(SemanticModel model) // it comes from modifier clean up AddInstruction(OpCode.RET); } - if (!context.Options.NoOptimize) + if (!_context.Options.NoOptimize) Optimizer.RemoveNops(_instructions); _startTarget.Instruction = _instructions[0]; } @@ -287,7 +287,7 @@ private void ProcessFieldInitializer(SemanticModel model, IFieldSymbol field, Ac Push(value.HexToBytes(true)); break; case ContractParameterType.Hash160: - Push((UInt160.TryParse(value, out var hash) ? hash : value.ToScriptHash(context.Options.AddressVersion)).ToArray()); + Push((UInt160.TryParse(value, out var hash) ? hash : value.ToScriptHash(_context.Options.AddressVersion)).ToArray()); break; case ContractParameterType.PublicKey: Push(ECPoint.Parse(value, ECCurve.Secp256r1).EncodePoint(true)); @@ -304,7 +304,6 @@ private void ProcessFieldInitializer(SemanticModel model, IFieldSymbol field, Ac } } - private IEnumerable<(byte fieldIndex, AttributeData attribute)> ConvertModifier(SemanticModel model) { foreach (var attribute in Symbol.GetAttributesWithInherited()) @@ -313,12 +312,12 @@ private void ProcessFieldInitializer(SemanticModel model, IFieldSymbol field, Ac continue; JumpTarget notNullTarget = new(); - byte fieldIndex = context.AddAnonymousStaticField(); + byte fieldIndex = _context.AddAnonymousStaticField(); AccessSlot(OpCode.LDSFLD, fieldIndex); AddInstruction(OpCode.ISNULL); Jump(OpCode.JMPIFNOT_L, notNullTarget); - MethodConvert constructor = context.ConvertMethod(model, attribute.AttributeConstructor!); + MethodConvert constructor = _context.ConvertMethod(model, attribute.AttributeConstructor!); CreateObject(model, attribute.AttributeClass, null); foreach (var arg in attribute.ConstructorArguments.Reverse()) Push(arg.Value); @@ -331,7 +330,7 @@ private void ProcessFieldInitializer(SemanticModel model, IFieldSymbol field, Ac var enterSymbol = attribute.AttributeClass.GetAllMembers() .OfType() .First(p => p.Name == nameof(ModifierAttribute.Enter) && p.Parameters.Length == 0); - MethodConvert enterMethod = context.ConvertMethod(model, enterSymbol); + MethodConvert enterMethod = _context.ConvertMethod(model, enterSymbol); EmitCall(enterMethod); yield return (fieldIndex, attribute); } @@ -342,7 +341,7 @@ private void ProcessFieldInitializer(SemanticModel model, IFieldSymbol field, Ac var exitSymbol = attribute.AttributeClass!.GetAllMembers() .OfType() .First(p => p.Name == nameof(ModifierAttribute.Exit) && p.Parameters.Length == 0); - MethodConvert exitMethod = context.ConvertMethod(model, exitSymbol); + MethodConvert exitMethod = _context.ConvertMethod(model, exitSymbol); if (exitMethod.IsEmpty) return null; var instruction = AccessSlot(OpCode.LDSFLD, fieldIndex); EmitCall(exitMethod); @@ -519,7 +518,7 @@ private void CreateObject(SemanticModel model, ITypeSymbol type, InitializerExpr IMethodSymbol[] virtualMethods = members.OfType().Where(p => p.IsVirtualMethod()).ToArray(); if (virtualMethods.Length > 0) { - byte index = context.AddVTable(type); + byte index = _context.AddVTable(type); AddInstruction(OpCode.DUP); AccessSlot(OpCode.LDSFLD, index); AddInstruction(OpCode.APPEND); diff --git a/src/Neo.Compiler.CSharp/MethodConvert/PropertyConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/PropertyConvert.cs index d3f9cb20b..4282b47b1 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/PropertyConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/PropertyConvert.cs @@ -45,7 +45,7 @@ private void ConvertFieldBackedProperty(IPropertySymbol property) if (Symbol.IsStatic) { IFieldSymbol backingField = Array.Find(fields, p => SymbolEqualityComparer.Default.Equals(p.AssociatedSymbol, property))!; - byte backingFieldIndex = context.AddStaticField(backingField); + byte backingFieldIndex = _context.AddStaticField(backingField); switch (Symbol.MethodKind) { case MethodKind.PropertyGet: @@ -185,7 +185,7 @@ private void ConvertStorageBackedProperty(IPropertySymbol property, AttributeDat if (Symbol.IsStatic) { IFieldSymbol backingField = Array.Find(fields, p => SymbolEqualityComparer.Default.Equals(p.AssociatedSymbol, property))!; - byte backingFieldIndex = context.AddStaticField(backingField); + byte backingFieldIndex = _context.AddStaticField(backingField); AccessSlot(OpCode.STSFLD, backingFieldIndex); } else diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index e9d9da172..8738fd365 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -18,6 +18,7 @@ using Neo.SmartContract.Manifest; using Neo.SmartContract.Testing.Extensions; using System; +using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; using System.CommandLine.NamingConventionBinder; @@ -51,7 +52,7 @@ static int Main(string[] args) return rootCommand.Invoke(args); } - private static void Handle(RootCommand command, Options options, string[] paths, InvocationContext context) + private static void Handle(RootCommand command, Options options, string[]? paths, InvocationContext context) { if (paths is null || paths.Length == 0) { @@ -127,15 +128,20 @@ private static int ProcessDirectory(Options options, string path) private static int ProcessCsproj(Options options, string path) { - return ProcessOutputs(options, Path.GetDirectoryName(path)!, CompilationContext.CompileProject(path, options)); + return ProcessOutputs(options, Path.GetDirectoryName(path)!, new CompilationEngine(options).CompileProject(path)); } private static int ProcessSources(Options options, string folder, string[] sourceFiles) { - return ProcessOutputs(options, folder, CompilationContext.CompileSources(sourceFiles, options)); + return ProcessOutputs(options, folder, new CompilationEngine(options).CompileSources(sourceFiles)); } - private static int ProcessOutputs(Options options, string folder, CompilationContext context) + private static int ProcessOutputs(Options options, string folder, List contexts) + { + return contexts.Select(p => ProcessOutput(options, folder, p)).Any(p => p != 1) ? 0 : 1; + } + + private static int ProcessOutput(Options options, string folder, CompilationContext context) { foreach (Diagnostic diagnostic in context.Diagnostics) { diff --git a/tests/Neo.Compiler.CSharp.TestContracts/Contract_Multiple.cs b/tests/Neo.Compiler.CSharp.TestContracts/Contract_Multiple.cs index 1aaf866c5..c668658a8 100644 --- a/tests/Neo.Compiler.CSharp.TestContracts/Contract_Multiple.cs +++ b/tests/Neo.Compiler.CSharp.TestContracts/Contract_Multiple.cs @@ -1,7 +1,3 @@ -using System; -using System.ComponentModel; -using System.Numerics; - namespace Neo.Compiler.CSharp.UnitTests.TestClasses { public class Contract_MultipleA : SmartContract.Framework.SmartContract diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest1.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest1.cs index 25f9e1930..9fe537788 100644 --- a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest1.cs +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest1.cs @@ -25,9 +25,11 @@ public void Test_Partial() public void Test_MultipleContracts() { var testengine = new TestEngine(); - var context = testengine.AddEntryScript(Utils.Extensions.TestContractRoot + "Contract_Multiple.cs"); - Assert.IsFalse(context.Success); - Assert.IsTrue(context.Diagnostics.Any(u => u.Id == DiagnosticId.MultiplyContracts)); + var contexts = testengine.AddEntryScripts(true, true, Utils.Extensions.TestContractRoot + "Contract_Multiple.cs"); + + Assert.IsTrue(contexts.Count == 2); + Assert.IsTrue(contexts.All(c => c.Success)); + Assert.IsTrue(contexts.All(c => c.Diagnostics.Count == 0)); } [TestMethod] diff --git a/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj b/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj index fe79fd307..5bc2a98e0 100644 --- a/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj +++ b/tests/Neo.SmartContract.Framework.UnitTests/Neo.SmartContract.Framework.UnitTests.csproj @@ -1,15 +1,15 @@ - - Neo.SmartContract.Framework.UnitTests - + + Neo.SmartContract.Framework.UnitTests + - - - - scfx - - - + + + + scfx + + + diff --git a/tests/Neo.SmartContract.TestEngine/TestEngine.cs b/tests/Neo.SmartContract.TestEngine/TestEngine.cs index 6d0ec3232..2912178b9 100644 --- a/tests/Neo.SmartContract.TestEngine/TestEngine.cs +++ b/tests/Neo.SmartContract.TestEngine/TestEngine.cs @@ -50,6 +50,12 @@ public TestEngine(TriggerType trigger = TriggerType.Application, IVerifiable? ve { } + /// + /// Though the compiler can compile multiple smart contract files, + /// only one smart contract context is returned. + /// + /// Source file path of the smart contracts + /// The first or default contract public CompilationContext AddEntryScript(params string[] files) { return AddEntryScript(true, true, files); @@ -60,16 +66,29 @@ public CompilationContext AddNoOptimizeEntryScript(params string[] files) return AddEntryScript(false, true, files); } + // TODO: Should not be hard to specify signer from here to enable contracts direct call. public CompilationContext AddEntryScript(bool optimize = true, bool debug = true, params string[] files) { - CompilationContext context = CompilationContext.Compile(files, references, new Options + return AddEntryScripts(optimize, debug, files).FirstOrDefault()!; + } + + public List AddEntryScripts(bool optimize = true, bool debug = true, params string[] files) + { + var contexts = new CompilationEngine(new Options { AddressVersion = ProtocolSettings.Default.AddressVersion, Debug = debug, NoOptimize = !optimize - }); - if (context.Success) + }).Compile(files, references); + + if (contexts == null || contexts.Count == 0) + { + throw new InvalidOperationException("No SmartContract is found in the sources."); + } + + if (contexts.All(p => p.Success)) { + var context = contexts.FirstOrDefault()!; Nef = context.CreateExecutable(); Manifest = context.CreateManifest(); DebugInfo = context.CreateDebugInformation(); @@ -77,9 +96,9 @@ public CompilationContext AddEntryScript(bool optimize = true, bool debug = true } else { - context.Diagnostics.ForEach(Console.Error.WriteLine); + contexts.ForEach(c => c.Diagnostics.ForEach(Console.Error.WriteLine)); } - return context; + return contexts; } public void Reset()