Files
FSI.BT.IR.Tools/Kalk/Kalk.Core/KalkEngine.cs
Maier Stephan SI b684704bf8 Sicherung
2023-01-20 16:09:00 +01:00

502 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Consolus;
using Scriban;
using Scriban.Functions;
using Scriban.Helpers;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine : TemplateContext
{
private readonly LexerOptions _lexerOptions;
private readonly LexerOptions _lexerInterpolatedOptions;
private readonly ParserOptions _parserOptions;
private object _lastResult = null;
private readonly ConsoleText _tempOutputHighlight;
private bool _hasResultOrVariableSet = false;
private Action _showInputAction = null;
private CancellationTokenSource _cancellationTokenSource;
private readonly bool _isInitializing;
private int _startIndexForCompletion;
private readonly List<string> _completionMatchingList;
private int _currentIndexInCompletionMatchingList;
private bool _isFirstWriteForEval;
private readonly Dictionary<Type, KalkModule> _modules;
private string _localClipboard;
private bool _formatting;
private readonly CultureInfo _integersCultureInfoWithUnderscore;
private KalkShortcutKeyMap _currentShortcutKeyMap;
public KalkEngine() : base(new KalkObjectWithAlias())
{
_integersCultureInfoWithUnderscore = new CultureInfo(CultureInfo.InvariantCulture.LCID)
{
NumberFormat =
{
NumberGroupSeparator = "_",
NumberDecimalDigits = 0
}
};
FileService = new DefaultFileService();
KalkSettings.Initialize();
KalkEngineFolder = AppContext.BaseDirectory;
// Enforce UTF8 encoding
// Console.OutputEncoding = Encoding.UTF8;
EnableEngineOutput = true;
EchoEnabled = true;
DisplayVersion = true;
CurrentDisplay = KalkDisplayMode.Standard;
KalkUserFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile, Environment.SpecialFolderOption.DoNotVerify), ".kalk");
HighlightOutput = new ConsoleText();
InputReader = Console.In;
OutputWriter = Console.Out;
ErrorWriter = Console.Error;
IsOutputSupportHighlighting = ConsoleHelper.SupportEscapeSequences;
// Fetch version
var assemblyInfoVersion = (AssemblyInformationalVersionAttribute)typeof(KalkEngine).Assembly.GetCustomAttribute(typeof(AssemblyInformationalVersionAttribute));
Version = assemblyInfoVersion.InformationalVersion;
Builtins = BuiltinObject;
((KalkObjectWithAlias)Builtins).Engine = this;
Units = new KalkUnits(this);
Shortcuts = new KalkShortcuts();
Aliases = new KalkAliases();
_currentShortcutKeyMap = Shortcuts.ShortcutKeyMap;
_completionMatchingList = new List<string>();
Config = new KalkConfig();
Variables = new ScriptVariables(this);
Descriptors = new Dictionary<string, KalkDescriptor>();
EnableRelaxedMemberAccess = false;
_modules = new Dictionary<Type, KalkModule>();
TryConverters = new List<TryToObjectDelegate>();
ErrorForStatementFunctionAsExpression = true;
StrictVariables = true;
UseScientific = true;
LoopLimit = int.MaxValue; // no limits for loops
RecursiveLimit = int.MaxValue; // no limits (still guarded by Scriban)
// Setup default clipboard methods
_localClipboard = string.Empty;
GetClipboardText = GetClipboardTextImpl;
SetClipboardText = SetClipboardTextImpl;
_cancellationTokenSource = new CancellationTokenSource();
PushGlobal(Units);
PushGlobal(Variables);
_parserOptions = new ParserOptions();
_lexerOptions = new LexerOptions()
{
KeepTrivia = true,
Mode = ScriptMode.ScriptOnly,
Lang = ScriptLang.Scientific
};
_lexerInterpolatedOptions = new LexerOptions()
{
KeepTrivia = true,
Mode = ScriptMode.Default,
Lang = ScriptLang.Scientific
};
_tempOutputHighlight = new ConsoleText();
// Init last result with 0
_lastResult = 0;
HistoryList = new List<string>();
_isInitializing = true;
RegisterFunctions();
_isInitializing = false;
}
private void SetClipboardTextImpl(string obj)
{
_localClipboard = obj ?? string.Empty;
}
private string GetClipboardTextImpl()
{
return _localClipboard;
}
private ConsoleText HighlightOutput { get; }
public IFileService FileService { get; set; }
public bool DisplayVersion { get; set; }
public string Version { get; }
public string KalkUserFolder { get; set; }
/// <summary>
/// The engine is in testing mode.
/// </summary>
public bool IsTesting { get; set; }
public string KalkEngineFolder { get; }
public Func<string, Stream> GetAppContentStream;
internal ConsoleTextWriter BufferedOutputWriter { get; private set; }
internal ConsoleTextWriter BufferedErrorWriter { get; private set; }
public TextReader InputReader { get; set; }
public TextWriter OutputWriter
{
get => BufferedOutputWriter?.Backend;
set => BufferedOutputWriter = value == null ? null : new ConsoleTextWriter(value);
}
public TextWriter ErrorWriter
{
get => BufferedErrorWriter?.Backend;
set => BufferedErrorWriter = value == null ? null : new ConsoleTextWriter(value);
}
public bool EchoEnabled { get; set; }
public bool EchoInput { get; set; }
public ScriptObject Builtins { get; }
/// <summary>
/// Gets the config object.
/// </summary>
/// <example>
/// ```kalk
/// >>> config
/// # config
/// out = {help_max_column: 100, limit_to_string: "auto"}
/// ```
/// </example>
[KalkExport("config", CategoryGeneral)]
public KalkConfig Config { get; }
public Action<string> SetClipboardText { get; set; }
public Func<string> GetClipboardText { get; set; }
public ScriptObject Variables { get; }
/// <summary>
/// Displays all built-in and user-defined aliases.
/// </summary>
/// <remarks>Aliases are usually used to define equivalent variable names for equivalent mathematical symbols. To create an alias, see the command `alias`.</remarks>
/// <example>
/// ```kalk
/// >>> aliases
/// # Builtin Aliases
/// alias(alpha, Α, α)
/// alias(beta, Β, β)
/// alias(chi, Χ, χ)
/// alias(delta, Δ, δ)
/// alias(epsilon, Ε, ε)
/// alias(eta, Η, η)
/// alias(gamma, Γ, γ)
/// alias(iota, Ι, ι)
/// alias(kappa, Κ, κ)
/// alias(lambda, Λ, λ)
/// alias(mu, Μ, μ)
/// alias(nu, Ν, ν)
/// alias(omega, Ω, ω)
/// alias(omicron, Ο, ο)
/// alias(phi, Φ, φ, ϕ)
/// alias(pi, Π, π)
/// alias(psi, Ψ, ψ)
/// alias(rho, Ρ, ρ)
/// alias(sigma, Σ, σ)
/// alias(tau, Τ, τ)
/// alias(theta, Θ, θ, ϑ)
/// alias(upsilon, Υ, υ)
/// alias(xi, Ξ, ξ)
/// alias(zeta, Ζ, ζ)
/// ```
/// </example>
[KalkExport("aliases", CategoryGeneral)]
public KalkAliases Aliases { get; }
private bool EnableEngineOutput { get; set; }
public bool IsOutputSupportHighlighting { get; set; }
public bool HasInteractiveConsole { get; private set; }
/// <summary>
/// Displays all built-in and user-defined keyboard shortcuts.
/// </summary>
/// <remarks>To create an keyboard shortcut, see the command `shortcut`.</remarks>
/// <example>
/// ```kalk
/// >>> clear shortcuts
/// >>> shortcut(tester, "ctrl+d", '"' + date + '"')
/// >>> shortcuts
/// # User-defined Shortcuts
/// shortcut(tester, "ctrl+d", '"' + date + '"') # ctrl+d => '"' + date + '"'
/// ```
/// </example>
[KalkExport("shortcuts", CategoryGeneral)]
public KalkShortcuts Shortcuts { get; }
public Dictionary<string, KalkDescriptor> Descriptors { get; }
public Action OnClearScreen { get; set; }
public bool HasExit { get; private set; }
public List<string> HistoryList { get; }
public List<TryToObjectDelegate> TryConverters { get; }
private static readonly Regex MatchHistoryRegex = new Regex(@"^\s*!(\d+)\s*");
public Template Parse(string text, string filePath = null, bool recordHistory = true, bool interpolated = false)
{
if (!TryParseSpecialHistoryBangCommand(text, out var template))
{
var lexerOptions = interpolated ? _lexerInterpolatedOptions : _lexerOptions;
if (filePath != null)
{
// Don't keep trivia when loading from a file as we are not going to format anything
// and it will make the parse errors correct (TODO: fix it when also parsing)
lexerOptions.KeepTrivia = false;
}
template = Template.Parse(text, filePath, parserOptions: _parserOptions, lexerOptions: lexerOptions);
}
if (recordHistory && text.Length != 0 && !template.HasErrors)
{
if (HistoryList.Count == 0 || HistoryList[^1] != text)
{
HistoryList.Add(text);
}
}
return template;
}
public object EvaluatePage(ScriptPage script)
{
_isFirstWriteForEval = true;
RecordInput(script);
return Evaluate(script);
}
public void RecordInput(ScriptPage toRewrite)
{
_hasResultOrVariableSet = false;
_showInputAction = () =>
{
var newScript = toRewrite.Format(new ScriptFormatterOptions(this, ScriptLang.Scientific, ScriptFormatterFlags.ExplicitClean));
var output = newScript.ToString();
WriteHighlightLine($"# {output}");
};
}
protected virtual void RecordSetVariable(string name, object value)
{
NotifyResultOrVariable();
WriteHighlightVariableAndValueToConsole(name, value);
}
private void SetLastResult(object textAsObject)
{
_lastResult = textAsObject;
NotifyResultOrVariable();
}
private void NotifyResultOrVariable()
{
if (_hasResultOrVariableSet) return;
_hasResultOrVariableSet = true;
_showInputAction?.Invoke();
_showInputAction = null;
}
private bool AcceptUnit(ScriptExpression leftExpression, string unitName)
{
return !(leftExpression is ScriptVariable variable) || variable.Name != "help";
}
public override TemplateContext Write(SourceSpan span, object textAsObject)
{
// If we are in formatting mode, we use the normal output
if (_formatting) return base.Write(span, textAsObject);
SetLastResult(textAsObject);
if (EnableEngineOutput && EchoEnabled)
{
WriteHighlightVariableAndValueToConsole("out", textAsObject);
}
return this;
}
public override string ObjectToString(object value, bool nested = false)
{
if (value is float f32)
{
if (float.IsNegativeInfinity(f32)) return "-inf";
if (float.IsInfinity(f32)) return "inf";
if (float.IsNaN(f32)) return "nan";
}
else if (value is double f64)
{
if (double.IsNegativeInfinity(f64)) return "-inf";
if (double.IsInfinity(f64)) return "inf";
if (double.IsNaN(f64)) return "nan";
}
else if (value is DateTime && _miscModule != null)
{
// Output DateTime only if we have the date builtin object accessible (that provides the implementation of the ToString method)
return _miscModule.DateObject.ToString((DateTime)value, _miscModule.DateObject.Format, CurrentCulture);
}
if (CurrentDisplay != KalkDisplayMode.Raw && (value is int | value is uint || value is ulong || value is long || value is BigInteger || value is short || value is ushort))
{
return ((IFormattable)value).ToString("N", _integersCultureInfoWithUnderscore);
}
return base.ObjectToString(value, nested);
}
protected override IObjectAccessor GetMemberAccessorImpl(object target)
{
if (target != null && target.GetType().IsPrimitiveOrDecimal())
{
return PrimitiveSwizzleAccessor.Default;
}
return base.GetMemberAccessorImpl(target);
}
private class ScriptVariables : KalkObjectWithAlias
{
public ScriptVariables(KalkEngine engine) : base(engine)
{
}
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
{
if (base.TrySetValue(context, span, member, value, readOnly))
{
if (Engine.EnableEngineOutput)
{
Engine.RecordSetVariable(member, value);
}
return true;
}
return false;
}
}
private class EmptyObject
{
public static readonly EmptyObject Instance = new EmptyObject();
public override string ToString() => string.Empty;
}
public override void Import(SourceSpan span, object objectToImport)
{
if (objectToImport is KalkModule module)
{
ImportModule(module);
return;
}
base.Import(span, objectToImport);
}
protected void ImportModule(KalkModule module)
{
module.InternalImport();
}
public T ToObject<T>(int argIndex, object value)
{
try
{
return ToObject<T>(CurrentSpan, value);
}
catch (ScriptRuntimeException ex)
{
throw new ScriptArgumentException(argIndex, ex.OriginalMessage);
}
catch (Exception ex)
{
throw new ScriptArgumentException(argIndex, ex.Message);
}
}
public object ToObject(int argIndex, object value, Type destinationType)
{
try
{
return ToObject(CurrentSpan, value, destinationType);
}
catch (ScriptRuntimeException ex)
{
throw new ScriptArgumentException(argIndex, ex.OriginalMessage);
}
catch (Exception ex)
{
throw new ScriptArgumentException(argIndex, ex.Message);
}
}
public override object ToObject(SourceSpan span, object value, Type destinationType)
{
var converters = TryConverters;
if (converters.Count > 0)
{
foreach (var converter in converters)
{
if (converter(span, value, destinationType, out var newValue))
{
return newValue;
}
}
}
if (destinationType == typeof(KalkHalf))
{
return KalkHalf.FromObject(this, span, value);
}
return base.ToObject(span, value, destinationType);
}
public override bool ToBool(SourceSpan span, object value)
{
if (value is KalkBool kb) { return kb; }
return base.ToBool(span, value);
}
}
public delegate bool TryToObjectDelegate(SourceSpan span, object value, Type destinationType, out object newValue);
}