File: DataDescriptorModel.cs
Web Access
Project: src\src\runtime\src\coreclr\tools\cdac-build-tool\cdac-build-tool.csproj (cdac-build-tool)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.DotNet.Diagnostics.DataContract.JsonConverter;

namespace Microsoft.DotNet.Diagnostics.DataContract.BuildTool;

public class DataDescriptorModel
{
    public int Version => 1;
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string Baseline { get; }
    public IReadOnlyDictionary<string, TypeModel> Types { get; }
    public IReadOnlyDictionary<string, GlobalModel> Globals { get; }
    public IReadOnlyDictionary<string, GlobalModel> SubDescriptors { get; }
    public IReadOnlyDictionary<string, string> Contracts { get; }
    [JsonIgnore]
    public uint PlatformFlags { get; }
    // The number of indirect globals plus 1 for the placeholder at index 0
    [JsonIgnore]
    public int PointerDataCount => 1 + Globals.Values.Count(g => g.Value.Kind == GlobalValue.KindEnum.Indirect) + SubDescriptors.Values.Count(s => s.Value.Kind == GlobalValue.KindEnum.Indirect);

    private DataDescriptorModel(string baseline, IReadOnlyDictionary<string, TypeModel> types, IReadOnlyDictionary<string, GlobalModel> globals, IReadOnlyDictionary<string, GlobalModel> subDescriptors, IReadOnlyDictionary<string, string> contracts, uint platformFlags)
    {
        Baseline = baseline;
        Types = types;
        Globals = globals;
        SubDescriptors = subDescriptors;
        Contracts = contracts;
        PlatformFlags = platformFlags;
    }

    public const string PointerTypeName = "pointer";
    public const string StringTypeName = "string";

    internal void DumpModel()
    {
        Console.WriteLine("\nData Descriptor Model:");
        Console.WriteLine($"Platform Flags: 0x{PlatformFlags:x8}");
        Console.WriteLine($"Baseline: {Baseline}");
        foreach (var (typeName, type) in Types)
        {
            Console.WriteLine($"Type: {typeName}");
            if (type.Size != null)
            {
                Console.WriteLine($"  Size: 0x{type.Size:x8}");
            }
            foreach (var (fieldName, field) in type.Fields)
            {
                Console.WriteLine($"  Field: {fieldName}");
                Console.WriteLine($"    Type: {field.Type}");
                Console.WriteLine($"    Offset: 0x{field.Offset:x8}");
            }
        }
        foreach (var (globalName, global) in Globals)
        {
            Console.WriteLine($"Global: {globalName}");
            Console.WriteLine($"  Type: {global.Type}");
            Console.WriteLine($"  Value: {global.Value}");
        }
        foreach (var (subDescriptorName, subDescriptor) in SubDescriptors)
        {
            Console.WriteLine($"Sub-Descriptor: {subDescriptorName}");
            Console.WriteLine($"  Type: {subDescriptor.Type}");
            Console.WriteLine($"  Value: {subDescriptor.Value}");
        }
        foreach (var (contractName, contract) in Contracts)
        {
            Console.WriteLine($"Contract: {contractName}");
            Console.WriteLine($"  Version: {contract}");
        }
    }

    private static JsonSerializerOptions s_jsonSerializerOptions = new JsonSerializerOptions
    {
        WriteIndented = false,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DictionaryKeyPolicy = null, // leave unchanged
    };
    public string ToJson()
    {
        // always writes the "compact" format, see data_descriptor.md
        return JsonSerializer.Serialize(this, s_jsonSerializerOptions);
    }

    public class Builder
    {
        private string _baseline;
        private readonly string _baselinesDir;
        private bool _baselineParsed;
        private readonly Dictionary<string, TypeModelBuilder> _types = new();
        private readonly Dictionary<string, GlobalBuilder> _globals = new();
        private readonly Dictionary<string, GlobalBuilder> _subDescriptors = new();
        private readonly Dictionary<string, ContractBuilder> _contracts = new();
        public Builder(string baselinesDir)
        {
            _baseline = string.Empty;
            _baselineParsed = false;
            _baselinesDir = baselinesDir;
        }

        public uint PlatformFlags {get; set;}

        public TypeModelBuilder AddOrUpdateType(string name, int? size)
        {
            if (!_baselineParsed)
            {
                throw new InvalidOperationException("Baseline must be set before adding types");
            }
            if (!_types.TryGetValue(name, out var type))
            {
                type = new TypeModelBuilder();
                _types[name] = type;
            }
            type.Size = size;
            return type;
        }

        public GlobalBuilder AddOrUpdateGlobal(string name, string type, GlobalValue? value)
        {
            if (!_baselineParsed)
            {
                throw new InvalidOperationException("Baseline must be set before adding globals");
            }
            if (!_globals.TryGetValue(name, out var global))
            {
                global = new GlobalBuilder();
                _globals[name] = global;
            }
            global.Type = type;
            global.Value = value;
            return global;
        }

        public GlobalBuilder AddOrUpdateSubDescriptor(string name, string type, GlobalValue? value)
        {
            if (!_baselineParsed)
            {
                throw new InvalidOperationException("Baseline must be set before adding globals");
            }
            if (!_subDescriptors.TryGetValue(name, out var subDescriptor))
            {
                subDescriptor = new GlobalBuilder();
                _subDescriptors[name] = subDescriptor;
            }
            subDescriptor.Type = type;
            subDescriptor.Value = value;
            return subDescriptor;
        }

        public void AddOrUpdateContract(string name, string version)
        {
            if (!_contracts.TryGetValue(name, out var contract))
            {
                contract = new ContractBuilder();
                _contracts[name] = contract;
            }
            contract.Version = version;
        }

        public void SetBaseline(string baseline)
        {
            if (_baseline != string.Empty && _baseline != baseline)
            {
                throw new InvalidOperationException($"Baseline already set to {_baseline} cannot set to {baseline}");
            }
            var baselines = new DirectoryBaselines(_baselinesDir);
            if (baselines.BaselineNames.Contains(baseline))
            {
                _baseline = baseline;
            }
            else
            {
                throw new InvalidOperationException($"Baseline '{baseline}' not known");
            }
            _baseline = baseline;
            if (!_baselineParsed)
            {
                _baselineParsed = true; // kind of a hack - set it before parsing the baseline, so we can call AddOrUpdateType
                ParseBaseline();
            }
        }

        private void ParseBaseline()
        {
            if (_baseline != "empty")
            {
                throw new InvalidOperationException("TODO: [cdac] - implement baseline parsing");
            }
        }

        public DataDescriptorModel Build()
        {
            var types = new Dictionary<string, TypeModel>();
            foreach (var (typeName, typeBuilder) in _types)
            {
                types[typeName] = typeBuilder.Build(typeName);
            }
            var globals = new Dictionary<string, GlobalModel>();
            foreach (var (globalName, globalBuilder) in _globals)
            {
                GlobalValue? v = globalBuilder.Value;
                if (v == null)
                {
                    throw new InvalidOperationException($"Value must be set for global {globalName}");
                }
                globals[globalName] = new GlobalModel { Type = globalBuilder.Type, Value = v.Value };
            }
            var subDescriptors = new Dictionary<string, GlobalModel>();
            foreach (var (subDescriptorName, subDescriptorBuilder) in _subDescriptors)
            {
                GlobalValue? v = subDescriptorBuilder.Value;
                if (v == null)
                {
                    throw new InvalidOperationException($"Value must be set for sub-descriptor {subDescriptorName}");
                }
                subDescriptors[subDescriptorName] = new GlobalModel { Type = subDescriptorBuilder.Type, Value = v.Value };
            }
            var contracts = new Dictionary<string, string>();
            foreach (var (contractName, contractBuilder) in _contracts)
            {
                contracts[contractName] = contractBuilder.Build();
            }
            return new DataDescriptorModel(_baseline, types, globals, subDescriptors, contracts, PlatformFlags);
        }
    }

    public class TypeModelBuilder
    {
        private readonly Dictionary<string, FieldBuilder> _fields = new();
        private int? _size;
        public TypeModelBuilder() { }

        public int? Size
        {
            get => _size;
            set
            {
                if (_size != null && (value == null || _size != (int)value))
                {
                    throw new InvalidOperationException($"Size already set to {_size} cannot set to {value}");
                }
                _size = value;
            }
        }

        public void AddOrUpdateField(string name, string type, int? offset)
        {
            if (!_fields.TryGetValue(name, out var field))
            {
                field = new FieldBuilder();
                _fields[name] = field;
            }
            field.Type = type;
            field.Offset = offset;
        }

        public TypeModel Build(string typeName)
        {
            var fields = new Dictionary<string, FieldModel>();
            foreach (var (fieldName, fieldBuilder) in _fields)
            {
                fields.Add(fieldName, fieldBuilder.Build(typeName, fieldName));
            }
            return new TypeModel { Size = _size, Fields = fields };
        }
    }

    public class GlobalBuilder
    {
        private string _type = string.Empty;
        private GlobalValue? _value;
        public string Type
        {
            get => _type;
            set
            {
                if (_type != string.Empty && _type != value)
                {
                    throw new InvalidOperationException($"Type already set to {_type} cannot set to {value}");
                }
                _type = value;
            }
        }
        public GlobalValue? Value
        {
            get => _value;
            set
            {
                if (_value != null && _value != value)
                {
                    throw new InvalidOperationException($"Value already set to {_value} cannot set to {value}");
                }
                _value = value;
            }
        }
    }

    internal sealed class FieldBuilder
    {
        private string _type = string.Empty;
        private int? _offset;
        public string Type
        {
            get => _type;
            set
            {
                if (_type != string.Empty && _type != value)
                {
                    throw new InvalidOperationException($"Type already set to {_type} cannot set to {value}");
                }
                _type = value;
            }
        }

        public int? Offset
        {
            get => _offset;
            set
            {
                if (_offset != null && (value == null || _offset != (int)value))
                {
                    throw new InvalidOperationException($"Offset already set to {_offset} cannot set to {value}");
                }
                _offset = value;
            }
        }

        public FieldModel Build(string typeName, string fieldName)
        {
            if (_offset == null)
            {
                throw new InvalidOperationException($"Offset must be set for {typeName}.{fieldName}");
            }
            return new FieldModel { Type = _type, Offset = (int)_offset };
        }
    }

    [JsonConverter(typeof(FieldModelJsonConverter))]
    public readonly struct FieldModel
    {
        public string Type { get; init; }
        public int Offset { get; init; }
    }

    [JsonConverter(typeof(TypeModelJsonConverter))]
    public readonly struct TypeModel
    {
        public int? Size { get; init; }
        public IReadOnlyDictionary<string, FieldModel> Fields { get; init; }
    }

    [JsonConverter(typeof(GlobalValueJsonConverter))]
    public readonly struct GlobalValue : IEquatable<GlobalValue>
    {
        public enum KindEnum
        {
            Direct,
            Indirect,
            String
        }

        public KindEnum Kind { get; private init; }
        public ulong NumericValue { get; }
        public string StringValue { get; }
        public static GlobalValue MakeDirect(ulong value) => new GlobalValue(value) { Kind = KindEnum.Direct };
        public static GlobalValue MakeIndirect(uint auxDataIdx) => new GlobalValue((ulong)auxDataIdx) { Kind = KindEnum.Indirect };
        public static GlobalValue MakeString(string value) => new GlobalValue(value) { Kind = KindEnum.String };
        private GlobalValue(ulong value) { NumericValue = value; StringValue = string.Empty;}
        private GlobalValue(string value) { StringValue = value; }

        public static bool operator ==(GlobalValue left, GlobalValue right) => left.Equals(right);
        public static bool operator !=(GlobalValue left, GlobalValue right) => !(left == right);

        public bool Equals(GlobalValue other) => other.Kind == Kind && other.NumericValue == NumericValue && other.StringValue == StringValue;
        public override bool Equals(object? obj) => obj is GlobalValue value && Equals(value);
        public override int GetHashCode() => HashCode.Combine(Kind, NumericValue, StringValue);
        public override string ToString()
        {
            return Kind switch
            {
                KindEnum.Direct => $"0x{NumericValue:x}",
                KindEnum.Indirect => $"Indirect({NumericValue})",
                KindEnum.String => $"'{StringValue}'",
                _ => throw new InvalidOperationException("Unknown GlobalValue type")
            };
        }
    }

    [JsonConverter(typeof(GlobalModelJsonConverter))]
    public readonly struct GlobalModel
    {
        public string Type { get; init; }
        public GlobalValue Value { get; init; }
    }

    public class ContractBuilder
    {
        private string? _version;
        public ContractBuilder()
        {
        }

        public string? Version
        {
            get => _version;
            set
            {
                if (_version is not null && _version != value)
                {
                    throw new InvalidOperationException($"Version already set to {_version} cannot set to {value}");
                }
                _version = value;
            }
        }

        // There is no ContractModel right now because the only info we keep is the version.
        // As a result it is convenient to use a Dictionary<string,string> for the contracts since
        // the JSON serialization coincides with what we want.
        public string Build()
        {
            if (_version is null)
            {
                throw new InvalidOperationException("Version must be set for contract");
            }
            return _version;
        }
    }
}