Skip to content

Instantly share code, notes, and snippets.

@jmcd
Created August 7, 2021 17:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmcd/b0bbc391ae708cfe090d7e27a4e065a5 to your computer and use it in GitHub Desktop.
Save jmcd/b0bbc391ae708cfe090d7e27a4e065a5 to your computer and use it in GitHub Desktop.
namespace OneHour
{
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
internal class Command
{
public enum CommandKind
{
SetVar,
GetVar,
PushVar,
Push,
Pop,
Add,
}
public static readonly Command Pop = new Command(CommandKind.Pop, default, default);
public static readonly Command Add = new Command(CommandKind.Add, default, default);
private Command(CommandKind kind, string? name, Value? value)
{
Kind = kind;
Name = name;
Value = value;
}
public CommandKind Kind { get; }
public string? Name { get; }
public Value? Value { get; }
public static Command SetVar(string name, Value value) => new Command(CommandKind.SetVar, name, value);
public static Command GetVar(string name) => new Command(CommandKind.GetVar, name, default);
public static Command PushVar(string name) => new Command(CommandKind.PushVar, name, default);
public static Command Push(Value value) => new Command(CommandKind.Push, default, value);
}
internal class Value
{
public enum ValueKind
{
Nothing,
String,
Int,
}
public static readonly Value Nothing = new Value(ValueKind.Nothing);
private Value(ValueKind kind, long? intValue = default, string? stringValue = default)
{
Kind = kind;
IntValue = intValue;
StringValue = stringValue;
}
public ValueKind Kind { get; }
public long? IntValue { get; }
public string? StringValue { get; }
public static Value String(string s) => new Value(ValueKind.String, default, s);
public static Value Int(long l) => new Value(ValueKind.Int, l);
}
internal enum Type { }
internal class EngineError
{
public static readonly EngineError MismatchType = new EngineError(Kind.MismatchType);
public static readonly EngineError MismatchNumParams = new EngineError(Kind.MismatchNumParams);
public static readonly EngineError EmptyStack = new EngineError(Kind.EmptyStack);
private readonly Kind kind;
private EngineError(Kind kind, string? unknownCommand = default, string? missingVariableName = default) => this.kind = kind;
public static EngineError UnknownCommand(string unknownCommand) => new EngineError(Kind.UnknownCommand, unknownCommand);
public static EngineError MissingVariableName(string missingVariableName) => new EngineError(Kind.UnknownCommand, default, missingVariableName);
private enum Kind
{
MismatchType,
MismatchNumParams,
UnknownCommand,
EmptyStack,
}
}
internal class Evaluator
{
private readonly Stack<Value> stack = new();
private readonly Dictionary<string, Value> vars = new();
public Result<Value, EngineError> Evaluate(IList<Command> commands)
{
var output = Result<Value, EngineError>.Ok(Value.Nothing);
foreach (var command in commands)
{
switch (command.Kind)
{
case Command.CommandKind.SetVar:
vars[command.Name!] = command.Value!;
break;
case Command.CommandKind.GetVar:
{
if (vars.TryGetValue(command.Name!, out var value))
{
output = Result<Value, EngineError>.Ok(value);
}
else
{
return Result<Value, EngineError>.Err(EngineError.MissingVariableName(command.Name!));
}
}
break;
case Command.CommandKind.PushVar:
{
if (vars.TryGetValue(command.Name!, out var value))
{
stack.Push(value);
}
else
{
return Result<Value, EngineError>.Err(EngineError.MissingVariableName(command.Name!));
}
}
break;
case Command.CommandKind.Push:
stack.Push(command.Value!);
break;
case Command.CommandKind.Pop:
output = Pop();
break;
case Command.CommandKind.Add:
var lhs = Pop();
if (lhs.ErrVal is object)
{
return Result<Value, EngineError>.Err(lhs.ErrVal);
}
var rhs = Pop();
if (rhs.ErrVal is object)
{
return Result<Value, EngineError>.Err(rhs.ErrVal);
}
var result = Add(lhs.OkVal!, rhs.OkVal!);
if (result.ErrVal is object)
{
return Result<Value, EngineError>.Err(result.ErrVal);
}
stack.Push(result.OkVal!);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return output;
}
private static Result<Value, EngineError> Add(Value lhs, Value rhs) =>
(lhs.Kind, rhs.Kind) switch
{
(Value.ValueKind.Int, Value.ValueKind.Int) => Result<Value, EngineError>.Ok(Value.Int(lhs.IntValue!.Value + rhs.IntValue!.Value)),
(Value.ValueKind.String, Value.ValueKind.String) => Result<Value, EngineError>.Ok(Value.String(lhs.StringValue! + rhs.StringValue!)),
_ => Result<Value, EngineError>.Err(EngineError.MismatchType),
};
private Result<Value, EngineError> Pop() =>
stack.TryPop(out var value) ? Result<Value, EngineError>.Ok(value) : Result<Value, EngineError>.Err(EngineError.EmptyStack);
}
internal class Result<TOk, TErr>
{
private Result(TOk? okVal, TErr? errVal)
{
OkVal = okVal;
ErrVal = errVal;
}
public TErr? ErrVal { get; }
public TOk? OkVal { get; }
public static Result<TOk, TErr> Ok(TOk ok) => new(ok, default);
public static Result<TOk, TErr> Err(TErr err) => new(default, err);
}
internal static class ParseUtils
{
public static Result<string, EngineError> ParseVarName(string varName) => Result<string, EngineError>.Ok(varName);
public static Result<Value, EngineError> ParseString(string val)
{
if (val.StartsWith('\"') && val.EndsWith('\"') && val.Length > 0)
{
var inner = val.Substring(1, val.Length - 2);
return Result<Value, EngineError>.Ok(Value.String(inner));
}
return Result<Value, EngineError>.Err(EngineError.MismatchType);
}
public static Result<Value, EngineError> ParseInt(string val)
{
if (long.TryParse(val, out var l))
{
return Result<Value, EngineError>.Ok(Value.Int(l));
}
return Result<Value, EngineError>.Err(EngineError.MismatchType);
}
public static Result<Value, EngineError> ParseValue(string val)
{
if (val.StartsWith('\"') && val.EndsWith('\"') && val.Length > 0)
{
return ParseString(val);
}
return ParseInt(val);
}
public static Result<Command, EngineError> ParseSet(IList<string> input)
{
if (input.Count != 3)
{
return Result<Command, EngineError>.Err(EngineError.MismatchNumParams);
}
var varName = ParseVarName(input[1]);
var value = ParseValue(input[2]);
if (varName.ErrVal != default)
{
return Result<Command, EngineError>.Err(varName.ErrVal);
}
if (value.ErrVal != default)
{
return Result<Command, EngineError>.Err(value.ErrVal);
}
return Result<Command, EngineError>.Ok(Command.SetVar(varName.OkVal!, value.OkVal!));
}
public static Result<Command, EngineError> ParseGet(IList<string> input)
{
if (input.Count != 2)
{
return Result<Command, EngineError>.Err(EngineError.MismatchNumParams);
}
var varName = ParseVarName(input[1]);
if (varName.ErrVal != default)
{
return Result<Command, EngineError>.Err(varName.ErrVal);
}
return Result<Command, EngineError>.Ok(Command.GetVar(varName.OkVal!));
}
public static Result<Command, EngineError> ParsePushVar(IList<string> input)
{
if (input.Count != 2)
{
return Result<Command, EngineError>.Err(EngineError.MismatchNumParams);
}
var varName = ParseVarName(input[1]);
if (varName.ErrVal != default)
{
return Result<Command, EngineError>.Err(varName.ErrVal);
}
return Result<Command, EngineError>.Ok(Command.PushVar(varName.OkVal!));
}
public static Result<Command, EngineError> ParsePush(IList<string> input)
{
if (input.Count != 2)
{
return Result<Command, EngineError>.Err(EngineError.MismatchNumParams);
}
var val = ParseValue(input[1]);
if (val.ErrVal != default)
{
return Result<Command, EngineError>.Err(val.ErrVal);
}
return Result<Command, EngineError>.Ok(Command.Push(val.OkVal!));
}
public static Result<IList<Command>, EngineError> Parse(string input)
{
var output = new List<Command>();
foreach (var line in input.Split('\n'))
{
string[] lineComponents = line.Split(' ');
var keyWord = lineComponents.FirstOrDefault();
Result<Command, EngineError>? result;
switch (keyWord)
{
case "set":
result = ParseSet(lineComponents);
break;
case "get":
result = ParseGet(lineComponents);
break;
case "push":
result = ParsePush(lineComponents);
break;
case "pushvar":
result = ParsePushVar(lineComponents);
break;
case "pop":
result = Result<Command, EngineError>.Ok(Command.Pop);
break;
case "add":
result = Result<Command, EngineError>.Ok(Command.Add);
break;
case null:
result = default;
break;
default:
return Result<IList<Command>, EngineError>.Err(EngineError.UnknownCommand(keyWord));
}
if (result?.OkVal != default)
{
output.Add(result.OkVal);
}
}
return Result<IList<Command>, EngineError>.Ok(output);
}
}
public class Tests
{
[Fact]
public void Test1()
{
var commands = new List<Command>
{
Command.SetVar("a", Value.Int(100)),
Command.GetVar("a"),
};
var evaluator = new Evaluator();
var result = evaluator.Evaluate(commands);
Assert.Equal(100, result.OkVal!.IntValue!.Value);
}
[Fact]
public void EvalSetGet()
{
const string input = "set x 30\nget x";
var commands = ParseUtils.Parse(input).OkVal!;
var evaluator = new Evaluator();
var result = evaluator.Evaluate(commands);
Assert.Equal(30, result.OkVal!.IntValue!.Value);
}
[Fact]
public void EvalStack()
{
const string input = "push 100\npush 30\nadd\npop";
var commands = ParseUtils.Parse(input).OkVal!;
var evaluator = new Evaluator();
var result = evaluator.Evaluate(commands);
Assert.Equal(130, result.OkVal!.IntValue!.Value);
}
[Fact]
public void EvalPushvar()
{
const string input = "set x 33\npushvar x\npush 100\nadd\npop";
var commands = ParseUtils.Parse(input).OkVal!;
var evaluator = new Evaluator();
var result = evaluator.Evaluate(commands);
Assert.Equal(133, result.OkVal!.IntValue!.Value);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment