using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading.Tasks; using SkiaSharp; int currentImageSize = 1024; string combinedOutputString = string.Empty; (bool success, double totalTimeTakenSecondsOutput) = BenchmarkFunction(() => { (float[] result, double timeTakenSecondsEvaluate) = BenchmarkFunction(() => { Instruction[] instructions = Parsing.Parse("Programs/prospero.vm"); return Interpreter.Evaluate(instructions, imageSize: currentImageSize); }); combinedOutputString += $" - took: {timeTakenSecondsEvaluate} seconds to evaluate the image!" + Environment.NewLine; (bool success, double timeTakenSecondsOutput) = BenchmarkFunction(() => { CreateOutputImage(currentImageSize, result, "prospero.jpg"); return true; }); combinedOutputString += $" - took: {timeTakenSecondsOutput} seconds to write out image!" + Environment.NewLine; return true; }); Console.Write($"Sharpero took: {totalTimeTakenSecondsOutput} seconds to evaluate {currentImageSize}x{currentImageSize} image!" + Environment.NewLine + combinedOutputString); (T, double) BenchmarkFunction(Func benchmarkedFunction) { var sw = Stopwatch.StartNew(); T benchmarkedResult = benchmarkedFunction(); return (benchmarkedResult, sw.Elapsed.TotalSeconds); } void CreateOutputImage(int imageSize, float[] imageData, string imageOutputPath) { byte[] imageDataBytes = imageData.Select(p => (byte)(p < 0 ? 255 : 0)).ToArray(); using var image = SKImage.FromPixelCopy(new SKImageInfo(imageSize, imageSize, SKColorType.Gray8), imageDataBytes); using var data = image.Encode(SKEncodedImageFormat.Jpeg, 100); using var stream = File.OpenWrite(imageOutputPath); data.SaveTo(stream); } internal enum OpCode { VarX, VarY, Const, Add, Sub, Mul, Max, Min, Neg, Square, Sqrt } internal readonly record struct Operand { private readonly int _value = -1; public bool IsConstant => ((_value >> 31) & 1) != 0; public int Value => _value & 0x7FFFFFFF; public Operand(int value, bool isConstant = false) { _value = (isConstant ? 1 : 0) << 31 | value & 0x7FFFFFFF; } public static implicit operator int(Operand o) => o.Value; public override string ToString() => $"{Value} (constant: {IsConstant.ToString().ToLower()})"; } internal readonly record struct Instruction( Operand Out, OpCode OpCode, Operand A = default, Operand B = default, float C = 0.0f) { public override string ToString() { return OpCode switch { OpCode.VarX => $"_{Out} var-x", OpCode.VarY => $"_{Out} var-y", OpCode.Const => $"_{Out} const", OpCode.Add => $"_{Out} add {A} {B}", OpCode.Sub => $"_{Out} sub {A} {B}", OpCode.Mul => $"_{Out} mul {A} {B}", OpCode.Max => $"_{Out} max {A} {B}", OpCode.Min => $"_{Out} min {A} {B}", OpCode.Neg => $"_{Out} neg {A}", OpCode.Square => $"_{Out} square {A}", OpCode.Sqrt => $"_{Out} sqrt {A}", _ => string.Empty }; } } internal static class Parsing { // 1D <-> 2D coordinate helpers for square grids public static (int x, int y) IndexToCoord(int idx, int width) => (idx % width, idx / width); public static int CoordToIndex(int x, int y, int width) => x + (y * width); // the identifiers are hexadecimal, stripping off the leading _ is enough :), public static Operand ParseIdentifier(Dictionary identifiers, string id, bool isConstant = false) { if (identifiers.TryGetValue(id, out var value)) { return value; } return identifiers[id] = new Operand(Convert.ToInt32(id[1..], 16), isConstant); } public static Instruction[] Parse(string filename) { var identifiers = new Dictionary(); foreach (string line in File.ReadAllLines(filename)) { switch (line.Split(" ")) { case [{ } @out, "const", not null]: ParseIdentifier(identifiers, @out, isConstant: true); break; case [{ } @out, not null, { } a, { } b]: ParseIdentifier(identifiers, @out); ParseIdentifier(identifiers, a); ParseIdentifier(identifiers, b); break; case [{ } @out, not null, { } a]: ParseIdentifier(identifiers, @out); ParseIdentifier(identifiers, a); break; case [{ } @out, not null]: ParseIdentifier(identifiers, @out); break; } } List instructions = []; foreach (string line in File.ReadAllLines(filename)) { Instruction? parsedInstruction = line.Split(" ") switch { [{ } @out, "var-x"] => new Instruction(identifiers[@out], OpCode.VarX), [{ } @out, "var-y"] => new Instruction(identifiers[@out], OpCode.VarY), [{ } @out, "const", { } v] => new Instruction(identifiers[@out], OpCode.Const, C: float.Parse(v, CultureInfo.InvariantCulture)), [{ } @out, "add", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Add, identifiers[a], identifiers[b]), [{ } @out, "sub", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Sub, identifiers[a], identifiers[b]), [{ } @out, "mul", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Mul, identifiers[a], identifiers[b]), [{ } @out, "max", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Max, identifiers[a], identifiers[b]), [{ } @out, "min", { } a, { } b] => new Instruction(identifiers[@out], OpCode.Min, identifiers[a], identifiers[b]), [{ } @out, "neg", { } a] => new Instruction(identifiers[@out], OpCode.Neg, identifiers[a]), [{ } @out, "square", { } a] => new Instruction(identifiers[@out], OpCode.Square, identifiers[a]), [{ } @out, "sqrt", { } a] => new Instruction(identifiers[@out], OpCode.Sqrt, identifiers[a]), _ => null }; if (parsedInstruction is { } instruction) { instructions.Add(instruction); } } float EvaluateExpression(OpCode opCode, float a, float b) { return opCode switch { OpCode.Add => a + b, OpCode.Sub => a - b, OpCode.Mul => a * b, _ => 0.0f }; } bool shouldEliminateConstants = false; if (shouldEliminateConstants) { // handle constant propagation, eliminating all constants from the instruction stream and merging them foreach (ref Instruction instruction in CollectionsMarshal.AsSpan(instructions)) { switch (instruction.OpCode) { case (OpCode.Add or OpCode.Sub or OpCode.Mul) when instruction.A.IsConstant || instruction.B.IsConstant: { instruction = instruction switch { { A.IsConstant: true, B.IsConstant: false } => instruction with { C = instructions[instruction.A].C }, { A.IsConstant: false, B.IsConstant: true } => instruction with { C = instructions[instruction.B].C }, { A.IsConstant: true, B.IsConstant: true } => instruction with { C = EvaluateExpression(instruction.OpCode, instructions[instruction.A].C, instructions[instruction.B].C) }, { A.IsConstant: false, B.IsConstant: false } => instruction }; break; } } } foreach (ref Instruction instruction in CollectionsMarshal.AsSpan(instructions)) { switch (instruction.OpCode) { case OpCode.Const: int offset = instruction.Out; foreach (ref Instruction otherInstruction in CollectionsMarshal.AsSpan(instructions)) { if (otherInstruction.Out >= offset) { otherInstruction = otherInstruction with { Out = new Operand(otherInstruction.Out - 1) }; } if (otherInstruction.A >= offset) { otherInstruction = otherInstruction with { A = new Operand(otherInstruction.A - 1, isConstant: otherInstruction.A.IsConstant) }; } if (otherInstruction.B >= offset) { otherInstruction = otherInstruction with { B = new Operand(otherInstruction.B - 1, otherInstruction.B.IsConstant) }; } } break; } } // eliminate all constants now :) int totalNumberOfInstructions = instructions.Count; int numberOfRemovedInstructions = instructions.RemoveAll(i => i.OpCode == OpCode.Const); Console.WriteLine($"Sharpero eliminated {numberOfRemovedInstructions} constants from tape, {(float)numberOfRemovedInstructions / totalNumberOfInstructions * 100.0f:0.0} % of total"); } return instructions.ToArray(); } } internal static class Compiler { static (AssemblyBuilder, MethodBuilder, TypeBuilder) CreateDynamicAssemblyWithMethodBuilder(string assemblyName, string moduleName, string typeName, string methodName) { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); var moduleBuilder = assemblyBuilder.DefineDynamicModule(moduleName); var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public); var methodBuilder = typeBuilder.DefineMethod( methodName, MethodAttributes.Public | MethodAttributes.Static, typeof(float), [typeof(float), typeof(float)]); // #TODO: how do this? // assemblyBuilder.EntryPoint = methodBuilder; return (assemblyBuilder, methodBuilder, typeBuilder); } static void BuildProgramToAssembly(Instruction[] instructions) { var (assemblyBuilder, methodBuilder, typeBuilder) = CreateDynamicAssemblyWithMethodBuilder( assemblyName: "SharperoAssembly", moduleName: "SharperoModule", typeName: "SharperoMain", methodName: "Evaluate"); } static void ExecuteProgramInAssembly() { } public static float[] Evaluate(Instruction[] instructions, int imageSize) { return Array.Empty(); } } internal static class Interpreter { public static float[] Evaluate(Instruction[] instructions, int imageSize) { float[] result = new float[imageSize * imageSize]; int chunkSize = Vector.Count; Parallel.For(0, (imageSize * imageSize) / chunkSize, chunkIdx => { Span xs = stackalloc float[chunkSize]; Span ys = stackalloc float[chunkSize]; for (int idx = 0; idx < chunkSize; ++idx) { int currentIdx = chunkIdx * chunkSize + idx; (int x, int y) = Parsing.IndexToCoord(currentIdx, width: imageSize); // fix up the coordinate space, our space is actually more like [-imageSize * 0.5f, imageSize * 0.5f], // ... rather than [0, imageSize] in x/y, so this gives us the expected result float vx = (x / (imageSize * 0.5f)) - 1.0f; float vy = 1.0f - (y / (imageSize * 0.5f)); (xs[idx], ys[idx]) = (vx, vy); } Vector results = Evaluate(instructions, new Vector(xs), new Vector(ys)); for (int idx = 0; idx < chunkSize; ++idx) { int currentIdx = chunkIdx * chunkSize + idx; (int x, int y) = Parsing.IndexToCoord(currentIdx, width: imageSize); result[Parsing.CoordToIndex(x, y, width: imageSize)] = results[idx]; } }); return result; } [SkipLocalsInit] public static Vector Evaluate(Instruction[] instructions, Vector xs, Vector ys) { // #TODO: this construction is just a little bit unhinged lol Span> variables = stackalloc Vector[instructions.Length]; foreach (ref Instruction instruction in instructions.AsSpan()) { variables[instruction.Out] = instruction switch { { OpCode: OpCode.VarX } => xs, { OpCode: OpCode.VarY } => ys, { OpCode: OpCode.Add, A: { IsConstant: false } a, B: { IsConstant: false } b } => variables[a] + variables[b], { OpCode: OpCode.Add, A.IsConstant: true, B: { IsConstant: false } b } => new Vector(instruction.C) + variables[b], { OpCode: OpCode.Add, A: { IsConstant: false } a, B.IsConstant: true } => variables[a] + new Vector(instruction.C), { OpCode: OpCode.Sub, A: { IsConstant: false } a, B: { IsConstant: false } b } => variables[a] - variables[b], { OpCode: OpCode.Sub, A.IsConstant: true, B: { IsConstant: false } b } => new Vector(instruction.C) - variables[b], { OpCode: OpCode.Sub, A: { IsConstant: false } a, B.IsConstant: true } => variables[a] - new Vector(instruction.C), { OpCode: OpCode.Mul, A: { IsConstant: false } a, B: { IsConstant: false } b } => variables[a] * variables[b], { OpCode: OpCode.Mul, A.IsConstant: true, B: { IsConstant: false } b } => new Vector(instruction.C) * variables[b], { OpCode: OpCode.Mul, A: { IsConstant: false } a, B.IsConstant: true } => variables[a] * new Vector(instruction.C), { OpCode: OpCode.Max, A: var a, B: var b } => Vector.Max(variables[a], variables[b]), { OpCode: OpCode.Min, A: var a, B: var b } => Vector.Min(variables[a], variables[b]), { OpCode: OpCode.Neg, A: var a } => -variables[a], { OpCode: OpCode.Sqrt, A: var a } => Vector.SquareRoot(variables[a]), { OpCode: OpCode.Square, A: var a } => variables[a] * variables[a], { OpCode: OpCode.Const, C: var v } => Vector.One * v, _ => variables[instruction.Out] }; } return variables[instructions.Length - 1]; } }