Files
RobustToolbox/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs
T
909fd326a0 Toolshed part 2 (#4256)
* Save work.

* three billion tweaks

* Rune-aware parser.

* a

* all shedded out for the night

* a

* oogh

* Publicizes a lot of common generic commands, so custom toolshed envs can include them.

* Implement parsing for all number types.

* i think i might implode

* a

* Tests.

* a

* Enum parser test.

* do u like parsers

* oopls

* ug fixes

* Toolshed is approaching a non-insignificant part of the engine's size.

* Pool toolshed's tests, also type tests.

* bwa

* tests pass :yay:

* Update Robust.Shared/CVars.cs

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>

* how did this not fail tests

* awa

* many levels of silly

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
2023-08-23 16:03:34 -05:00

328 lines
12 KiB
C#

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
using Invocable = Func<CommandInvocationArguments, object?>;
public sealed class ParsedCommand
{
public ToolshedCommand Command { get; }
public Type? ReturnType { get; }
public Type? PipedType => Bundle.PipedArgumentType;
internal Invocable Invocable { get; }
internal CommandArgumentBundle Bundle { get; }
public string? SubCommand { get; }
public static bool TryParse(
bool doAutoComplete,
ParserContext parserContext,
Type? pipedArgumentType,
[NotNullWhen(true)] out ParsedCommand? result,
out IConError? error,
out bool noCommand,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete,
Type? targetType = null
)
{
noCommand = false;
var checkpoint = parserContext.Save();
var bundle = new CommandArgumentBundle()
{Arguments = new(), Inverted = false, PipedArgumentType = pipedArgumentType, TypeArguments = Array.Empty<Type>()};
autocomplete = null;
if (!TryDigestModifiers(parserContext, bundle, out error)
|| !TryParseCommand(doAutoComplete, parserContext, bundle, pipedArgumentType, targetType, out var subCommand, out var invocable, out var command, out error, out noCommand, out autocomplete)
|| !command.TryGetReturnType(subCommand, pipedArgumentType, bundle.TypeArguments, out var retType)
)
{
result = null;
parserContext.Restore(checkpoint);
return false;
}
result = new(bundle, invocable, command, retType, subCommand);
return true;
}
private ParsedCommand(CommandArgumentBundle bundle, Invocable invocable, ToolshedCommand command, Type? returnType, string? subCommand)
{
Invocable = invocable;
Bundle = bundle;
Command = command;
ReturnType = returnType;
SubCommand = subCommand;
}
private static bool TryDigestModifiers(ParserContext parserContext, CommandArgumentBundle bundle, out IConError? error)
{
error = null;
if (parserContext.PeekWord() == "not")
{
parserContext.GetWord(); //yum
bundle.Inverted = true;
}
return true;
}
private static bool TryParseCommand(
bool makeCompletions,
ParserContext parserContext,
CommandArgumentBundle bundle,
Type? pipedType,
Type? targetType,
out string? subCommand,
[NotNullWhen(true)] out Invocable? invocable,
[NotNullWhen(true)] out ToolshedCommand? command,
out IConError? error,
out bool noCommand,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete
)
{
noCommand = false;
var start = parserContext.Index;
var cmd = parserContext.GetWord(ParserContext.IsCommandToken);
subCommand = null;
invocable = null;
command = null;
if (cmd is null)
{
if (parserContext.PeekRune() is null)
{
noCommand = true;
error = new OutOfInputError();
error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index));
autocomplete = null;
if (makeCompletions)
{
var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
return false;
}
else
{
noCommand = true;
error = new NotValidCommandError(targetType);
error.Contextualize(parserContext.Input, (start, parserContext.Index+1));
autocomplete = null;
return false;
}
}
if (!parserContext.Environment.TryGetCommand(cmd, out var cmdImpl))
{
error = new UnknownCommandError(cmd);
error.Contextualize(parserContext.Input, (start, parserContext.Index));
autocomplete = null;
if (makeCompletions)
{
var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
return false;
}
if (cmdImpl.HasSubCommands)
{
error = null;
autocomplete = null;
if (makeCompletions)
{
var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void)).Where(x => x.Cmd.Name == cmd);
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((
CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
if (parserContext.GetChar() is not ':')
{
error = new OutOfInputError();
error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index));
return false;
}
var subCmdStart = parserContext.Index;
if (parserContext.GetWord(ParserContext.IsToken) is not { } subcmd)
{
error = new OutOfInputError();
error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index));
return false;
}
if (!cmdImpl.Subcommands.Contains(subcmd))
{
error = new UnknownSubcommandError(cmd, subcmd, cmdImpl);
error.Contextualize(parserContext.Input, (subCmdStart, parserContext.Index));
return false;
}
subCommand = subcmd;
}
if (parserContext.ConsumeWhitespace() == 0 && makeCompletions)
{
error = null;
var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), null));
return false;
}
var argsStart = parserContext.Index;
if (!cmdImpl.TryParseArguments(makeCompletions, parserContext, pipedType, subCommand, out var args, out var types, out error, out autocomplete))
{
error?.Contextualize(parserContext.Input, (argsStart, parserContext.Index));
return false;
}
bundle.TypeArguments = types;
if (!cmdImpl.TryGetImplementation(bundle.PipedArgumentType, subCommand, types, out var impl))
{
error = new NoImplementationError(cmd, types, subCommand, bundle.PipedArgumentType, parserContext.Environment);
error.Contextualize(parserContext.Input, (start, parserContext.Index));
autocomplete = null;
return false;
}
bundle.Arguments = args;
invocable = impl;
command = cmdImpl;
autocomplete = null;
return true;
}
private bool _passedInvokeTest = false;
public object? Invoke(object? pipedIn, IInvocationContext ctx)
{
if (!_passedInvokeTest && !ctx.CheckInvokable(new CommandSpec(Command, SubCommand), out var error))
{
// Could not invoke the command for whatever reason, i.e. permission errors.
if (error is not null)
ctx.ReportError(error);
return null;
}
// TODO: This optimization might be dangerous if blocks can be passed to other people through vars.
// Or not if it can only be done deliberately, but social engineering is a thing.
_passedInvokeTest = true;
try
{
return Invocable.Invoke(new CommandInvocationArguments()
{Bundle = Bundle, PipedArgument = pipedIn, Context = ctx});
}
catch (Exception e)
{
ctx.ReportError(new UnhandledExceptionError(e));
return null;
}
}
}
public record struct UnknownCommandError(string Cmd) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup($"Got unknown command {Cmd}.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record NoImplementationError(string Cmd, Type[] Types, string? SubCommand, Type? PipedType, ToolshedEnvironment ctx) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = FormattedMessage.FromMarkup($"Could not find an implementation for {Cmd} given the input type {PipedType?.PrettyName() ?? "void"}.");
msg.PushNewline();
var typeArgs = "";
if (Types.Length != 0)
{
typeArgs = "<" + string.Join(",", Types.Select(ReflectionExtensions.PrettyName)) + ">";
}
msg.AddText($"Signature: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {PipedType?.PrettyName() ?? "void"} -> ???");
var piped = PipedType ?? typeof(void);
var cmdImpl = ctx.GetCommand(Cmd);
var accepted = cmdImpl.AcceptedTypes(SubCommand).ToHashSet();
foreach (var (command, subCommand) in ctx.CommandsTakingType(piped))
{
if (!command.TryGetReturnType(subCommand, piped, Array.Empty<Type>(), out var retType) || !accepted.Any(x => retType.IsAssignableTo(x)))
continue;
if (!cmdImpl.TryGetReturnType(SubCommand, retType, Types, out var myRetType))
continue;
msg.PushNewline();
msg.AddText($"The command {command.Name}{(subCommand is not null ? $":{subCommand}" : "")} can convert from {piped.PrettyName()} to {retType.PrettyName()}.");
msg.PushNewline();
msg.AddText($"With this fix, the new signature will be: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {retType?.PrettyName() ?? "void"} -> {myRetType?.PrettyName() ?? "void"}.");
}
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record UnknownSubcommandError(string Cmd, string SubCmd, ToolshedCommand Command) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = new FormattedMessage();
msg.AddText($"The command group {Cmd} doesn't have command {SubCmd}.");
msg.PushNewline();
msg.AddText($"The valid commands are: {string.Join(", ", Command.Subcommands)}.");
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record NotValidCommandError(Type? TargetType) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = new FormattedMessage();
msg.AddText("Ran into an invalid command, could not parse.");
if (TargetType is not null && TargetType != typeof(void))
{
msg.PushNewline();
msg.AddText($"The parser was trying to obtain a run of type {TargetType.PrettyName()}, make sure your run actually returns that value.");
}
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}