Sicherung

This commit is contained in:
Maier Stephan SI
2023-01-20 16:09:00 +01:00
parent e5257d8413
commit b684704bf8
139 changed files with 95678 additions and 499 deletions

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace Consolus
{
public struct ConsoleChar
{
public ConsoleChar(char value)
{
Value = value;
StyleMarkers = null;
}
public List<ConsoleStyleMarker> StyleMarkers;
public char Value;
public static implicit operator ConsoleChar(char c)
{
return new ConsoleChar(c);
}
public override string ToString()
{
return Value.ToString();
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Consolus
{
public class ConsoleHelper
{
public static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly Lazy<bool> _isConsoleSupportingEscapeSequences = new Lazy<bool>(IsConsoleSupportingEscapeSequencesInternal);
private static readonly Lazy<bool> _hasInteractiveConsole = new Lazy<bool>(HasInteractiveConsoleInternal);
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public static bool SupportEscapeSequences => _isConsoleSupportingEscapeSequences.Value;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public static bool HasInteractiveConsole => _hasInteractiveConsole.Value;
private static bool IsConsoleSupportingEscapeSequencesInternal()
{
if (Console.IsOutputRedirected || Console.IsErrorRedirected || !HasInteractiveConsole)
{
return false;
}
if (IsWindows)
{
WindowsHelper.EnableAnsiEscapeOnWindows();
}
var left = Console.CursorLeft;
var top = Console.CursorTop;
ConsoleStyle.Reset.Render(Console.Out);
bool support = true;
if (left != Console.CursorLeft || top != Console.CursorTop)
{
support = false;
// Remove previous sequence
Console.SetCursorPosition(left, top);
Console.Write(" "); // Clear \x1b[0m = 4 characters
Console.SetCursorPosition(left, top);
}
return support;
}
private static bool HasInteractiveConsoleInternal() => IsWindows ? WindowsHelper.HasConsoleWindows() : Environment.UserInteractive;
}
}

View File

@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
namespace Consolus
{
public class ConsoleRenderer
{
private readonly ConsoleTextWriter _consoleWriter;
private int _previousDisplayLength;
private int _cursorIndex;
private int _beforeEditLineSize;
public ConsoleRenderer()
{
SupportEscapeSequences = ConsoleHelper.SupportEscapeSequences;
_consoleWriter = new ConsoleTextWriter(Console.Out);
Prompt = new ConsoleText();
EditLine = new ConsoleText();
AfterEditLine = new ConsoleText();
SelectionIndex = -1;
EnableCursorChanged = true;
}
public bool SupportEscapeSequences { get; set; }
public ConsoleTextWriter ConsoleWriter => _consoleWriter;
public int LineTop
{
get
{
var currentLine = (_beforeEditLineSize + CursorIndex) / Console.BufferWidth;
return Console.CursorTop - currentLine;
}
}
public bool IsClean => _previousDisplayLength == 0;
public bool EnableCursorChanged { get; set; }
public int CursorIndex
{
get => _cursorIndex;
set
{
if (value < 0 || value > EditLine.Count) throw new ArgumentOutOfRangeException(nameof(value), $"Value must be >= 0 and <= {EditLine.Count}");
var lineTop = LineTop;
bool changed = _cursorIndex != value;
_cursorIndex = value;
UpdateCursorPosition(lineTop);
if (changed)
{
NotifyCursorChanged();
}
}
}
public Action CursorChanged { get; set; }
private void NotifyCursorChanged()
{
var cursorChanged = CursorChanged;
if (EnableCursorChanged) CursorChanged?.Invoke();
}
public bool HasSelection => SelectionIndex >= 0 && CursorIndex != SelectionIndex;
public int SelectionStartIndex => HasSelection ? SelectionIndex < CursorIndex ? SelectionIndex : CursorIndex : -1;
public int SelectionEndIndex => HasSelection ? SelectionIndex < CursorIndex ? CursorIndex : SelectionIndex : -1;
public int SelectionIndex { get; private set; }
public ConsoleText Prompt { get; }
public ConsoleText EditLine { get; }
public ConsoleText AfterEditLine { get; }
public Action BeforeRender { get; set; }
public void BeginSelection()
{
if (SelectionIndex >= 0) return;
SelectionIndex = CursorIndex;
}
public void EndSelection()
{
if (SelectionIndex < 0) return;
SelectionIndex = -1;
Render();
}
public void RemoveSelection()
{
if (!HasSelection) return;
var start = SelectionStartIndex;
var count = SelectionEndIndex - start;
for (int i = 0; i < count; i++)
{
if (start >= EditLine.Count) break;
EditLine.RemoveAt(start);
}
var cursorIndex = start;
SelectionIndex = -1;
Render(cursorIndex);
}
public void Reset()
{
AfterEditLine.Clear();
EditLine.Clear();
_cursorIndex = 0;
_beforeEditLineSize = 0;
_previousDisplayLength = 0;
}
public void Render(int? newCursorIndex = null, bool reset = false)
{
// Invoke before rendering to update the highlighting
BeforeRender?.Invoke();
Console.CursorVisible = false;
Console.SetCursorPosition(0, LineTop);
if (SupportEscapeSequences)
{
ConsoleStyle.Reset.Render(_consoleWriter);
}
else
{
Console.ResetColor();
}
Prompt.Render(_consoleWriter);
EditLine.SelectionStart = SelectionStartIndex;
EditLine.SelectionEnd = SelectionEndIndex;
EditLine.Render(_consoleWriter, SupportEscapeSequences);
AfterEditLine.Render(_consoleWriter);
// Fill remaining space with space
var newLength = _consoleWriter.VisibleCharacterCount;
var visualLength = newLength;
if (newLength < _previousDisplayLength)
{
if (SupportEscapeSequences)
{
ConsoleStyle.Reset.Render(_consoleWriter);
}
for (int i = newLength; i < _previousDisplayLength; i++)
{
_consoleWriter.Write(' ');
}
visualLength = _previousDisplayLength;
}
if (SupportEscapeSequences)
{
ConsoleStyle.Reset.Render(_consoleWriter);
}
_consoleWriter.Commit();
if (!SupportEscapeSequences)
{
Console.ResetColor();
}
_previousDisplayLength = newLength;
if (!reset)
{
// Calculate the current line based on the visual
var lineTop = Console.CursorTop - (visualLength - 1) / Console.BufferWidth;
_beforeEditLineSize = EditLine.VisibleCharacterStart;
if (newCursorIndex.HasValue)
{
bool changed = _cursorIndex != newCursorIndex.Value;
_cursorIndex = newCursorIndex.Value;
if (changed)
{
NotifyCursorChanged();
}
}
if (_cursorIndex > EditLine.Count) _cursorIndex = EditLine.Count;
UpdateCursorPosition(lineTop);
}
else
{
Reset();
}
Console.CursorVisible = true;
}
private void UpdateCursorPosition(int lineTop)
{
var position = _beforeEditLineSize + _cursorIndex;
var deltaX = position % Console.BufferWidth;
var deltaY = position / Console.BufferWidth;
Console.SetCursorPosition(deltaX, lineTop + deltaY);
}
}
}

View File

@@ -0,0 +1,750 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace Consolus
{
public class ConsoleRepl : ConsoleRenderer
{
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
private int _stackIndex;
private readonly BlockingCollection<ConsoleKeyInfo> _keys;
private Thread _thread;
public static bool IsSelf()
{
if (ConsoleHelper.IsWindows)
{
return WindowsHelper.IsSelfConsoleWindows();
}
return false;
}
public ConsoleRepl()
{
HasInteractiveConsole = ConsoleHelper.HasInteractiveConsole;
Prompt.Append(">>> ");
_stackIndex = -1;
History = new List<string>();
ExitOnNextEval = false;
PendingTextToEnter = new Queue<string>();
_keys = new BlockingCollection<ConsoleKeyInfo>();
if (HasInteractiveConsole)
{
Console.TreatControlCAsInput = true;
Console.CursorVisible = true;
}
}
public bool HasInteractiveConsole { get; }
public bool ExitOnNextEval { get; set; }
public Func<CancellationTokenSource> GetCancellationTokenSource { get; set; }
public PreProcessKeyDelegate TryPreProcessKey { get; set; }
public delegate bool PreProcessKeyDelegate(ConsoleKeyInfo key, ref int cursorIndex);
public Action<ConsoleKeyInfo> PostProcessKey { get; set; }
public List<string> History { get; }
public Action<string> SetClipboardTextImpl { get; set; }
public Func<string> GetClipboardTextImpl { get; set; }
public string LocalClipboard { get; set; }
public Func<string, bool, bool> OnTextValidatingEnter { get; set; }
public Action<string> OnTextValidatedEnter { get; set; }
private Queue<string> PendingTextToEnter { get; }
public bool Evaluating { get; private set; }
public void Begin()
{
CursorIndex = 0;
Render();
}
public void End()
{
CursorIndex = EditLine.Count;
Render();
}
public void EnqueuePendingTextToEnter(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
PendingTextToEnter.Enqueue(text);
}
public void UpdateSelection()
{
if (SelectionIndex >= 0)
{
Render();
}
}
private static UnicodeCategory GetCharCategory(char c)
{
if (c == '_' || (c >= '0' && c <= '9')) return UnicodeCategory.LowercaseLetter;
c = char.ToLowerInvariant(c);
return char.GetUnicodeCategory(c);
}
public void MoveLeft(bool word = false)
{
var cursorIndex = CursorIndex;
if (cursorIndex > 0)
{
if (word)
{
UnicodeCategory? category = null;
// Remove any space before
while (cursorIndex > 0)
{
cursorIndex--;
var newCategory = GetCharCategory(EditLine[cursorIndex].Value);
if (newCategory != UnicodeCategory.SpaceSeparator)
{
category = newCategory;
break;
}
}
while (cursorIndex > 0)
{
cursorIndex--;
var newCategory = GetCharCategory(EditLine[cursorIndex].Value);
if (category.HasValue)
{
if (newCategory != category.Value)
{
cursorIndex++;
break;
}
}
else
{
category = newCategory;
}
}
}
else
{
cursorIndex--;
}
}
CursorIndex = cursorIndex;
Render();
}
private int FindNextWordRight(int cursorIndex)
{
var category = GetCharCategory(EditLine[cursorIndex].Value);
while (cursorIndex < EditLine.Count)
{
var newCategory = GetCharCategory(EditLine[cursorIndex].Value);
if (newCategory != category)
{
break;
}
cursorIndex++;
}
while (cursorIndex < EditLine.Count)
{
var newCategory = GetCharCategory(EditLine[cursorIndex].Value);
if (newCategory != UnicodeCategory.SpaceSeparator)
{
break;
}
cursorIndex++;
}
return cursorIndex;
}
public void MoveRight(bool word = false)
{
var cursorIndex = CursorIndex;
if (cursorIndex < EditLine.Count)
{
if (word)
{
cursorIndex = FindNextWordRight(cursorIndex);
}
else
{
cursorIndex++;
}
}
CursorIndex = cursorIndex;
Render();
}
private void CopySelectionToClipboard()
{
if (!HasSelection) return;
var text = new StringBuilder();
var from = SelectionIndex < CursorIndex ? SelectionIndex : CursorIndex;
var to = SelectionIndex < CursorIndex ? CursorIndex - 1 : SelectionIndex - 1;
for (int i = from; i <= to; i++)
{
text.Append(EditLine[i].Value);
}
bool useLocalClipboard = true;
var textToClip = text.ToString();
if (SetClipboardTextImpl != null)
{
try
{
SetClipboardTextImpl(textToClip);
useLocalClipboard = false;
}
catch
{
// ignore
}
}
if (useLocalClipboard)
{
LocalClipboard = textToClip;
}
}
public string GetClipboardText()
{
if (LocalClipboard != null) return LocalClipboard;
if (GetClipboardTextImpl != null)
{
try
{
return GetClipboardTextImpl();
}
catch
{
// ignore
}
}
return null;
}
private void Backspace(bool word)
{
if (HasSelection)
{
RemoveSelection();
return;
}
var cursorIndex = CursorIndex;
if (cursorIndex == 0) return;
if (word)
{
MoveLeft(true);
var newCursorIndex = CursorIndex;
var length = cursorIndex - CursorIndex;
EditLine.RemoveRangeAt(newCursorIndex, length);
cursorIndex = newCursorIndex;
}
else
{
cursorIndex--;
EditLine.RemoveAt(cursorIndex);
}
Render(cursorIndex);
}
private void Delete(bool word)
{
if (HasSelection)
{
RemoveSelection();
return;
}
var cursorIndex = CursorIndex;
if (cursorIndex < EditLine.Count)
{
if (word)
{
var count = FindNextWordRight(cursorIndex) - cursorIndex;
EditLine.RemoveRangeAt(cursorIndex, count);
}
else
{
EditLine.RemoveAt(cursorIndex);
}
Render();
}
}
public void SetLine(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
EndSelection();
// Don't update if it is already empty
if (EditLine.Count == 0 && string.IsNullOrEmpty(text)) return;
EditLine.ReplaceBy(text);
Render(EditLine.Count);
}
public void Write(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
Write(text, 0, text.Length);
}
public void Write(string text, int index, int length)
{
if (text == null) throw new ArgumentNullException(nameof(text));
var cursorIndex = CursorIndex + length;
EditLine.InsertRange(CursorIndex, text, index, length);
Render(cursorIndex);
}
public void Write(ConsoleStyle style)
{
var cursorIndex = CursorIndex;
EditLine.EnableStyleAt(cursorIndex, style);
Render(cursorIndex);
}
public void Write(char c)
{
if (SelectionIndex >= 0)
{
RemoveSelection();
}
EndSelection();
var cursorIndex = CursorIndex;
EditLine.Insert(cursorIndex, c);
cursorIndex++;
Render(cursorIndex);
}
private bool Enter(bool force)
{
if (EnterInternal(force))
{
while (PendingTextToEnter.Count > 0)
{
var newTextToEnter = PendingTextToEnter.Dequeue();
EditLine.Clear();
EditLine.Append(newTextToEnter);
if (!EnterInternal(force)) return false;
}
return true;
}
return false;
}
private bool EnterInternal(bool hasControl)
{
if (HasSelection)
{
EndSelection();
}
End();
var text = EditLine.Count == 0 ? string.Empty : EditLine.ToString();
// Try to validate the string
if (OnTextValidatingEnter != null)
{
bool isValid = false;
try
{
Evaluating = true;
isValid = OnTextValidatingEnter(text, hasControl);
}
finally
{
Evaluating = false;
}
if (!isValid)
{
Render();
return false;
}
}
bool isNotEmpty = !IsClean || EditLine.Count > 0 || AfterEditLine.Count > 0;
if (isNotEmpty)
{
Render(reset: true);
}
// Propagate enter validation
OnTextValidatedEnter?.Invoke(text);
if (!string.IsNullOrEmpty(text))
{
History.Add(text);
}
_stackIndex = -1;
if (!ExitOnNextEval)
{
Render(0);
}
return true;
}
public void Clear()
{
Reset();
Console.Clear();
}
private void Exit()
{
End();
Render(reset: true);
Console.Write($"{ConsoleStyle.BrightRed}^Z");
Console.ResetColor();
if (!ConsoleHelper.IsWindows)
{
Console.WriteLine();
}
ExitOnNextEval = true;
}
private static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public void Run()
{
if (IsWindows)
{
// Clear any previous running thread
if (_thread != null)
{
try
{
_thread.Abort();
}
catch
{
// ignore
}
_thread = null;
}
_thread = new Thread(ThreadReadKeys) {IsBackground = true, Name = "Consolus.ThreadReadKeys"};
_thread.Start();
}
_stackIndex = -1;
// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
Render();
while (!ExitOnNextEval)
{
try
{
ConsoleKeyInfo key;
key = IsWindows ? _keys.Take() : Console.ReadKey(true);
ProcessKey(key);
}
catch (Exception ex)
{
AfterEditLine.Clear();
AfterEditLine.Append("\n");
AfterEditLine.Begin(ConsoleStyle.Red);
AfterEditLine.Append(ex.Message);
Render(); // re-display the current line with the exception
// Display the next line
//Render();
}
}
}
private void ThreadReadKeys()
{
while (!ExitOnNextEval)
{
var key = Console.ReadKey(true);
if (Evaluating && (key.Modifiers & ConsoleModifiers.Control) != 0 && key.Key == ConsoleKey.C)
{
GetCancellationTokenSource()?.Cancel();
}
else
{
_keys.Add(key);
}
}
}
private void PasteClipboard()
{
var clipboard = GetClipboardText();
if (clipboard != null)
{
clipboard = clipboard.TrimEnd();
int previousIndex = 0;
while (true)
{
int matchIndex = clipboard.IndexOf('\n', previousIndex);
int index = matchIndex;
bool exit = false;
if (index < 0)
{
index = clipboard.Length;
exit = true;
}
while (index > 0 && index < clipboard.Length && clipboard[index - 1] == '\r')
{
index--;
}
Write(clipboard, previousIndex, index - previousIndex);
if (exit)
{
break;
}
else
{
previousIndex = matchIndex + 1;
// Otherwise we have a new line
Enter(true);
}
}
}
}
protected virtual void ProcessKey(ConsoleKeyInfo key)
{
_isStandardAction = false;
_hasShift = (key.Modifiers & ConsoleModifiers.Shift) != 0;
// Only support selection if we have support for escape sequences
if (SupportEscapeSequences && _hasShift)
{
BeginSelection();
}
// Try to pre-process key
var cursorIndex = CursorIndex;
if (TryPreProcessKey != null && TryPreProcessKey(key, ref cursorIndex))
{
if (!_isStandardAction)
{
if (SelectionIndex >= 0)
{
RemoveSelection();
}
EndSelection();
Render(cursorIndex);
}
}
else if (key.KeyChar >= ' ')
{
Write(key.KeyChar);
}
// Remove selection if shift is no longer selected
if (!_hasShift)
{
EndSelection();
}
// Post-process key
if (PostProcessKey != null)
{
PostProcessKey(key);
}
}
private void DebugCursorPosition(string text = null)
{
Console.Title = $"x:{Console.CursorLeft} y:{Console.CursorTop} (Size w:{Console.BufferWidth} h:{Console.BufferHeight}){(text == null ? string.Empty: " " + text)}";
}
private void GoHistory(bool next)
{
var newStackIndex = _stackIndex + (next ? -1 : 1);
if (newStackIndex < 0 || newStackIndex >= History.Count)
{
if (newStackIndex < 0)
{
SetLine(string.Empty);
_stackIndex = -1;
}
}
else
{
_stackIndex = newStackIndex;
var index = (History.Count - 1) - _stackIndex;
SetLine(History[index]);
}
}
private bool _hasShift;
private bool _isStandardAction;
public void Action(ConsoleAction action)
{
_isStandardAction = true;
switch (action)
{
case ConsoleAction.Exit:
Exit();
break;
case ConsoleAction.CursorLeft:
MoveLeft(false);
break;
case ConsoleAction.CursorRight:
MoveRight(false);
break;
case ConsoleAction.CursorLeftWord:
MoveLeft(true);
break;
case ConsoleAction.CursorRightWord:
MoveRight(true);
break;
case ConsoleAction.CursorStartOfLine:
Begin();
break;
case ConsoleAction.CursorEndOfLine:
End();
break;
case ConsoleAction.HistoryPrevious:
GoHistory(false);
break;
case ConsoleAction.HistoryNext:
GoHistory(true);
break;
case ConsoleAction.DeleteCharacterLeft:
Backspace(false);
_stackIndex = -1;
_hasShift = false;
break;
case ConsoleAction.DeleteCharacterLeftAndCopy:
break;
case ConsoleAction.DeleteCharacterRight:
Delete(false);
_stackIndex = -1;
_hasShift = false;
break;
case ConsoleAction.DeleteCharacterRightAndCopy:
break;
case ConsoleAction.DeleteWordLeft:
Backspace(true);
_stackIndex = -1;
_hasShift = false;
break;
case ConsoleAction.DeleteWordRight:
Delete(true);
_stackIndex = -1;
_hasShift = false;
break;
case ConsoleAction.Completion:
break;
case ConsoleAction.DeleteTextRightAndCopy:
break;
case ConsoleAction.DeleteWordRightAndCopy:
break;
case ConsoleAction.DeleteWordLeftAndCopy:
break;
case ConsoleAction.CopySelection:
CopySelectionToClipboard();
break;
case ConsoleAction.CutSelection:
CopySelectionToClipboard();
RemoveSelection();
break;
case ConsoleAction.PasteClipboard:
PasteClipboard();
break;
case ConsoleAction.ValidateLine:
Enter(false);
break;
case ConsoleAction.ForceValidateLine:
Enter(true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(action), action, $"Invalid action {action}");
}
}
}
public enum ConsoleAction
{
Exit,
CursorLeft,
CursorRight,
CursorLeftWord,
CursorRightWord,
CursorStartOfLine,
CursorEndOfLine,
HistoryPrevious,
HistoryNext,
DeleteCharacterLeft,
DeleteCharacterLeftAndCopy,
DeleteCharacterRight,
DeleteCharacterRightAndCopy,
DeleteWordLeft,
DeleteWordRight,
Completion,
DeleteTextRightAndCopy,
DeleteWordRightAndCopy,
DeleteWordLeftAndCopy,
CopySelection,
CutSelection,
PasteClipboard,
ValidateLine,
ForceValidateLine,
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Consolus
{
public enum ConsoleStyleKind
{
Color,
Format,
Reset,
}
public readonly struct ConsoleStyle : IEquatable<ConsoleStyle>
{
/// https://en.wikipedia.org/wiki/ANSI_escape_code
public static readonly ConsoleStyle Black = new ConsoleStyle("\u001b[30m");
public static readonly ConsoleStyle Red = new ConsoleStyle("\u001b[31m");
public static readonly ConsoleStyle Green = new ConsoleStyle("\u001b[32m");
public static readonly ConsoleStyle Yellow = new ConsoleStyle("\u001b[33m");
public static readonly ConsoleStyle Blue = new ConsoleStyle("\u001b[34m");
public static readonly ConsoleStyle Magenta = new ConsoleStyle("\u001b[35m");
public static readonly ConsoleStyle Cyan = new ConsoleStyle("\u001b[36m");
public static readonly ConsoleStyle White = new ConsoleStyle("\u001b[37m");
public static readonly ConsoleStyle BrightBlack = new ConsoleStyle("\u001b[30;1m");
public static readonly ConsoleStyle BrightRed = new ConsoleStyle("\u001b[31;1m");
public static readonly ConsoleStyle BrightGreen = new ConsoleStyle("\u001b[32;1m");
public static readonly ConsoleStyle BrightYellow = new ConsoleStyle("\u001b[33;1m");
public static readonly ConsoleStyle BrightBlue = new ConsoleStyle("\u001b[34;1m");
public static readonly ConsoleStyle BrightMagenta = new ConsoleStyle("\u001b[35;1m");
public static readonly ConsoleStyle BrightCyan = new ConsoleStyle("\u001b[36;1m");
public static readonly ConsoleStyle BrightWhite = new ConsoleStyle("\u001b[37;1m");
public static readonly ConsoleStyle BackgroundBlack = new ConsoleStyle("\u001b[40m");
public static readonly ConsoleStyle BackgroundRed = new ConsoleStyle("\u001b[41m");
public static readonly ConsoleStyle BackgroundGreen = new ConsoleStyle("\u001b[42m");
public static readonly ConsoleStyle BackgroundYellow = new ConsoleStyle("\u001b[43m");
public static readonly ConsoleStyle BackgroundBlue = new ConsoleStyle("\u001b[44m");
public static readonly ConsoleStyle BackgroundMagenta = new ConsoleStyle("\u001b[45m");
public static readonly ConsoleStyle BackgroundCyan = new ConsoleStyle("\u001b[46m");
public static readonly ConsoleStyle BackgroundWhite = new ConsoleStyle("\u001b[47m");
public static readonly ConsoleStyle BackgroundBrightBlack = new ConsoleStyle("\u001b[40;1m");
public static readonly ConsoleStyle BackgroundBrightRed = new ConsoleStyle("\u001b[41;1m");
public static readonly ConsoleStyle BackgroundBrightGreen = new ConsoleStyle("\u001b[42;1m");
public static readonly ConsoleStyle BackgroundBrightYellow = new ConsoleStyle("\u001b[43;1m");
public static readonly ConsoleStyle BackgroundBrightBlue = new ConsoleStyle("\u001b[44;1m");
public static readonly ConsoleStyle BackgroundBrightMagenta = new ConsoleStyle("\u001b[45;1m");
public static readonly ConsoleStyle BackgroundBrightCyan = new ConsoleStyle("\u001b[46;1m");
public static readonly ConsoleStyle BackgroundBrightWhite = new ConsoleStyle("\u001b[47;1m");
public static readonly ConsoleStyle Bold = new ConsoleStyle("\u001b[1m", ConsoleStyleKind.Format);
public static readonly ConsoleStyle Underline = new ConsoleStyle("\u001b[4m", ConsoleStyleKind.Format);
public static readonly ConsoleStyle Reversed = new ConsoleStyle("\u001b[7m", ConsoleStyleKind.Format);
public static readonly ConsoleStyle Reset = new ConsoleStyle("\u001b[0m", ConsoleStyleKind.Reset);
private static readonly Dictionary<string, ConsoleStyle> MapKnownEscapeToConsoleStyle = new Dictionary<string, ConsoleStyle>(StringComparer.OrdinalIgnoreCase);
static ConsoleStyle()
{
MapKnownEscapeToConsoleStyle.Add(Black.EscapeSequence, Black);
MapKnownEscapeToConsoleStyle.Add(Red.EscapeSequence, Red);
MapKnownEscapeToConsoleStyle.Add(Green.EscapeSequence, Green);
MapKnownEscapeToConsoleStyle.Add(Yellow.EscapeSequence, Yellow);
MapKnownEscapeToConsoleStyle.Add(Blue.EscapeSequence, Blue);
MapKnownEscapeToConsoleStyle.Add(Magenta.EscapeSequence, Magenta);
MapKnownEscapeToConsoleStyle.Add(Cyan.EscapeSequence, Cyan);
MapKnownEscapeToConsoleStyle.Add(White.EscapeSequence, White);
MapKnownEscapeToConsoleStyle.Add(BrightBlack.EscapeSequence, BrightBlack);
MapKnownEscapeToConsoleStyle.Add(BrightRed.EscapeSequence, BrightRed);
MapKnownEscapeToConsoleStyle.Add(BrightGreen.EscapeSequence, BrightGreen);
MapKnownEscapeToConsoleStyle.Add(BrightYellow.EscapeSequence, BrightYellow);
MapKnownEscapeToConsoleStyle.Add(BrightBlue.EscapeSequence, BrightBlue);
MapKnownEscapeToConsoleStyle.Add(BrightMagenta.EscapeSequence, BrightMagenta);
MapKnownEscapeToConsoleStyle.Add(BrightCyan.EscapeSequence, BrightCyan);
MapKnownEscapeToConsoleStyle.Add(BrightWhite.EscapeSequence, BrightWhite);
MapKnownEscapeToConsoleStyle.Add(BackgroundBlack.EscapeSequence, BackgroundBlack);
MapKnownEscapeToConsoleStyle.Add(BackgroundRed.EscapeSequence, BackgroundRed);
MapKnownEscapeToConsoleStyle.Add(BackgroundGreen.EscapeSequence, BackgroundGreen);
MapKnownEscapeToConsoleStyle.Add(BackgroundYellow.EscapeSequence, BackgroundYellow);
MapKnownEscapeToConsoleStyle.Add(BackgroundBlue.EscapeSequence, BackgroundBlue);
MapKnownEscapeToConsoleStyle.Add(BackgroundMagenta.EscapeSequence, BackgroundMagenta);
MapKnownEscapeToConsoleStyle.Add(BackgroundCyan.EscapeSequence, BackgroundCyan);
MapKnownEscapeToConsoleStyle.Add(BackgroundWhite.EscapeSequence, BackgroundWhite);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightBlack.EscapeSequence, BackgroundBrightBlack);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightRed.EscapeSequence, BackgroundBrightRed);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightGreen.EscapeSequence, BackgroundBrightGreen);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightYellow.EscapeSequence, BackgroundBrightYellow);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightBlue.EscapeSequence, BackgroundBrightBlue);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightMagenta.EscapeSequence, BackgroundBrightMagenta);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightCyan.EscapeSequence, BackgroundBrightCyan);
MapKnownEscapeToConsoleStyle.Add(BackgroundBrightWhite.EscapeSequence, BackgroundBrightWhite);
MapKnownEscapeToConsoleStyle.Add(Bold.EscapeSequence, Bold);
MapKnownEscapeToConsoleStyle.Add(Underline.EscapeSequence, Underline);
MapKnownEscapeToConsoleStyle.Add(Reversed.EscapeSequence, Reversed);
MapKnownEscapeToConsoleStyle.Add(Reset.EscapeSequence, Reset);
}
public ConsoleStyle(string escapeSequence) : this()
{
EscapeSequence = escapeSequence;
Kind = ConsoleStyleKind.Color;
}
public ConsoleStyle(string escapeSequence, ConsoleStyleKind kind) : this()
{
EscapeSequence = escapeSequence;
Kind = kind;
}
private ConsoleStyle(string escapeSequence, ConsoleStyleKind kind, bool isInline)
{
EscapeSequence = escapeSequence;
Kind = kind;
IsInline = isInline;
}
public static ConsoleStyle Inline(string escape, ConsoleStyleKind kind = ConsoleStyleKind.Color)
{
if (escape == null) throw new ArgumentNullException(nameof(escape));
if (kind == ConsoleStyleKind.Color)
{
if (MapKnownEscapeToConsoleStyle.TryGetValue(escape, out var style))
{
kind = style.Kind;
}
}
return new ConsoleStyle(escape, kind, true);
}
public static ConsoleStyle Rgb(int r, int g, int b)
{
return new ConsoleStyle($"\x1b[38;2;{r};{g};{b}m", ConsoleStyleKind.Color);
}
public static ConsoleStyle BackgroundRgb(int r, int g, int b)
{
return new ConsoleStyle($"\x1b[48;2;{r};{g};{b}m", ConsoleStyleKind.Color);
}
public readonly string EscapeSequence;
public readonly ConsoleStyleKind Kind;
public readonly bool IsInline;
public void Render(TextWriter writer)
{
writer.Write(EscapeSequence);
}
#if NETSTANDARD2_0
public static implicit operator ConsoleStyle(ConsoleColor color)
{
switch (color)
{
case ConsoleColor.Black:
return Black;
case ConsoleColor.Blue:
return BrightBlue;
case ConsoleColor.Cyan:
return BrightCyan;
case ConsoleColor.DarkBlue:
return Blue;
case ConsoleColor.DarkCyan:
return Cyan;
case ConsoleColor.DarkGray:
return BrightBlack;
case ConsoleColor.DarkGreen:
return Green;
case ConsoleColor.DarkMagenta:
return Magenta;
case ConsoleColor.DarkRed:
return Red;
case ConsoleColor.DarkYellow:
return Yellow;
case ConsoleColor.Gray:
return BrightBlack;
case ConsoleColor.Green:
return BrightGreen;
case ConsoleColor.Magenta:
return BrightMagenta;
case ConsoleColor.Red:
return BrightRed;
case ConsoleColor.White:
return BrightWhite;
case ConsoleColor.Yellow:
return BrightYellow;
default:
throw new ArgumentOutOfRangeException(nameof(color), color, null);
}
}
#endif
public bool Equals(ConsoleStyle other)
{
return EscapeSequence == other.EscapeSequence && Kind == other.Kind && IsInline == other.IsInline;
}
public override bool Equals(object obj)
{
return obj is ConsoleStyle other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
return ((((EscapeSequence?.GetHashCode() ?? 0) * 397) ^ (int) Kind) * 397) & IsInline.GetHashCode();
}
}
public static bool operator ==(ConsoleStyle left, ConsoleStyle right)
{
return left.Equals(right);
}
public static bool operator !=(ConsoleStyle left, ConsoleStyle right)
{
return !left.Equals(right);
}
public override string ToString()
{
return EscapeSequence;
}
}
}

View File

@@ -0,0 +1,716 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Consolus
{
using static ConsoleStyle;
public struct ConsoleStyleMarker
{
public ConsoleStyleMarker(ConsoleStyle style, bool enabled)
{
Style = style;
Enabled = enabled;
}
public readonly ConsoleStyle Style;
public readonly bool Enabled;
public static implicit operator ConsoleStyleMarker(ConsoleStyle style)
{
return new ConsoleStyleMarker(style, true);
}
}
public class ConsoleText : IList<ConsoleChar>
{
private readonly List<ConsoleChar> _chars;
private readonly List<ConsoleStyleMarker> _leadingStyles;
private readonly List<ConsoleStyleMarker> _trailingStyles;
private bool _changedCalled;
private readonly StringBuilder _currentEscape;
private EscapeState _escapeState;
public ConsoleText()
{
_chars = new List<ConsoleChar>();
_leadingStyles = new List<ConsoleStyleMarker>();
_trailingStyles = new List<ConsoleStyleMarker>();
SelectionEnd = -1;
_currentEscape = new StringBuilder();
}
public ConsoleText(string text) : this()
{
Append(text);
}
public Action Changed { get; set; }
public int VisibleCharacterStart { get; internal set; }
public int VisibleCharacterEnd { get; internal set; }
public int SelectionStart { get; set; }
public int SelectionEnd { get; set; }
public void Add(ConsoleChar item)
{
Insert(Count, item);
}
public void Clear()
{
ResetAndSet(null);
NotifyChanged();
}
private void ResetAndSet(string text)
{
_leadingStyles.Clear();
_chars.Clear();
_trailingStyles.Clear();
SelectionStart = 0;
SelectionEnd = -1;
if (text != null)
{
foreach (var c in text)
{
_chars.Add(c);
}
}
}
public void ReplaceBy(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
ResetAndSet(text);
NotifyChanged();
}
public void ClearSelection()
{
SelectionStart = 0;
SelectionEnd = -1;
}
public bool HasSelection => SelectionStart >= 0 && SelectionEnd <= Count && SelectionStart <= SelectionEnd;
public void ClearStyles()
{
_leadingStyles.Clear();
_trailingStyles.Clear();
for (var i = 0; i < _chars.Count; i++)
{
var consoleChar = _chars[i];
if (consoleChar.StyleMarkers != null)
{
consoleChar.StyleMarkers.Clear();
_chars[i] = consoleChar;
}
}
}
public bool ClearStyle(ConsoleStyle style)
{
var removed = RemoveStyle(style, _leadingStyles);
removed = RemoveStyle(style, _trailingStyles) || removed;
for (var i = 0; i < _chars.Count; i++)
{
var consoleChar = _chars[i];
if (consoleChar.StyleMarkers != null)
{
removed = RemoveStyle(style, consoleChar.StyleMarkers) || removed;
}
}
return removed;
}
private static bool RemoveStyle(ConsoleStyle style, List<ConsoleStyleMarker> markers)
{
bool styleRemoved = false;
if (markers == null) return false;
for (var i = markers.Count - 1; i >= 0; i--)
{
var consoleStyleMarker = markers[i];
if (consoleStyleMarker.Style == style)
{
markers.RemoveAt(i);
styleRemoved = true;
}
}
return styleRemoved;
}
public bool Contains(ConsoleChar item)
{
return _chars.Contains(item);
}
public void CopyTo(ConsoleChar[] array, int arrayIndex)
{
_chars.CopyTo(array, arrayIndex);
}
public bool Remove(ConsoleChar item)
{
return _chars.Remove(item);
}
public int Count => _chars.Count;
public bool IsReadOnly => false;
public int IndexOf(ConsoleChar item)
{
return _chars.IndexOf(item);
}
public void Insert(int index, ConsoleChar item)
{
InsertInternal(index, item);
NotifyChanged();
}
public void Insert(int index, string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
for (int i = 0; i < text.Length; i++)
{
InsertInternal(index + i, text[i]);
}
NotifyChanged();
}
private enum EscapeState
{
None,
Escape,
EscapeCsiParameterBytes,
EscapeCsiIntermediateBytes,
EscapeCsiFinalByte,
}
private void InsertInternal(int index, ConsoleChar item)
{
if (item.Value == '\x1b')
{
_currentEscape.Append(item.Value);
_escapeState = EscapeState.Escape;
return;
}
if (_escapeState != EscapeState.None)
{
bool isCharInvalidValid = false;
// All sequences start with followed by a char in the range 0x400x5F: (ASCII @AZ[\]^_)
var c = item.Value;
if (_escapeState >= EscapeState.EscapeCsiParameterBytes)
{
// CSI: ESC [ followed by
// - by any number (including none) of "parameter bytes", char in the 0x300x3F: (ASCII 09:;<=>?)
// - then by any number of "intermediate bytes" in the range 0x200x2F (ASCII space and !"#$%&'()*+,-./),
// - then finally by a single "final byte" in the range 0x400x7E (ASCII @AZ[\]^_`az{|}~)
switch (_escapeState)
{
case EscapeState.EscapeCsiParameterBytes:
if (c >= 0x30 && c <= 0x3F)
{
_currentEscape.Append(c);
}
else
{
goto case EscapeState.EscapeCsiIntermediateBytes;
}
break;
case EscapeState.EscapeCsiIntermediateBytes:
if (c >= 0x20 && c <= 0x2F)
{
_escapeState = EscapeState.EscapeCsiIntermediateBytes;
_currentEscape.Append(c);
}
else
{
goto case EscapeState.EscapeCsiFinalByte;
}
break;
case EscapeState.EscapeCsiFinalByte:
if (c >= 0x40 && c <= 0x7E)
{
_currentEscape.Append(c);
}
else
{
isCharInvalidValid = true;
}
var styleAsText = _currentEscape.ToString();
_currentEscape.Length = 0;
_escapeState = EscapeState.None;
InsertInternal(index, new ConsoleStyleMarker(Inline(styleAsText), true));
break;
}
}
else
{
if (_currentEscape.Length == 1)
{
_currentEscape.Append(c);
if (c == '[')
{
_escapeState = EscapeState.EscapeCsiParameterBytes;
}
else
{
var styleAsText = _currentEscape.ToString();
_currentEscape.Length = 0;
_escapeState = EscapeState.None;
InsertInternal(index, new ConsoleStyleMarker(Inline(styleAsText), true));
}
}
}
if (!isCharInvalidValid)
{
return;
}
// otherwise the character hasn't been consumed, so we propagate it as a real char.
}
// Copy any leading/trailing escapes
bool isFirstInsert = index == 0 && Count == 0;
bool isLastInsert = Count > 0 && index == Count;
List<ConsoleStyleMarker> copyFrom = isFirstInsert ? _leadingStyles : isLastInsert ? _trailingStyles : null;
if (copyFrom != null && copyFrom.Count > 0)
{
var escapes = item.StyleMarkers;
if (escapes == null)
{
escapes = new List<ConsoleStyleMarker>();
item.StyleMarkers = escapes;
}
for (int i = 0; i < copyFrom.Count; i++)
{
escapes.Insert(i, copyFrom[i]);
}
copyFrom.Clear();
}
_chars.Insert(index, item);
}
public void InsertRange(int index, string text, int textIndex, int length)
{
if (text == null) throw new ArgumentNullException(nameof(text));
var cursorIndex = index;
var end = textIndex + length;
for (int i = textIndex; i < end; i++)
{
var c = text[i];
InsertInternal(cursorIndex, c);
cursorIndex++;
}
NotifyChanged();
}
private void NotifyChanged()
{
var changed = Changed;
// Avoid recursive change
if (changed != null && !_changedCalled)
{
_changedCalled = true;
try
{
changed();
}
finally
{
_changedCalled = false;
}
}
}
public void RemoveAt(int index)
{
_chars.RemoveAt(index);
NotifyChanged();
}
public void RemoveRangeAt(int index, int length)
{
for (int i = 0; i < length; i++)
{
_chars.RemoveAt(index);
}
NotifyChanged();
}
public ConsoleChar this[int index]
{
get => _chars[index];
set => _chars[index] = value;
}
public void Add(ConsoleStyle style)
{
Add(style, true);
}
public void Add(ConsoleStyle style, bool enabled)
{
if (enabled) EnableStyleAt(Count, style);
else DisableStyleAt(Count, style);
}
public ConsoleText Append(char c)
{
Add(c);
return this;
}
public ConsoleText Begin(ConsoleStyle style)
{
Add(style);
return this;
}
public ConsoleText End(ConsoleStyle style)
{
Add(style, false);
return this;
}
public ConsoleText AppendLine(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
AddInternal(text);
Add('\n');
return this;
}
public ConsoleText AppendLine()
{
Add('\n');
return this;
}
public void AddRange(ConsoleText text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
foreach(var c in text._leadingStyles)
{
InsertInternal(Count, c);
}
foreach(var c in text._chars)
{
InsertInternal(Count, c);
}
foreach (var c in text._trailingStyles)
{
InsertInternal(Count, c);
}
}
public ConsoleText Append(ConsoleStyle style, bool enabled)
{
Add(style, enabled);
return this;
}
public ConsoleText Append(string text)
{
if (text == null) throw new ArgumentNullException(nameof(text));
AddInternal(text);
NotifyChanged();
return this;
}
private void AddInternal(string text)
{
foreach (var c in text)
{
InsertInternal(Count, c);
}
}
public void EnableStyleAt(int index, ConsoleStyle style)
{
InsertInternal(index, style);
}
public void DisableStyleAt(int index, ConsoleStyle style)
{
InsertInternal(index, new ConsoleStyleMarker(style, false));
}
private void InsertInternal(int index, ConsoleStyleMarker marker)
{
if ((uint)index > (uint)Count) throw new ArgumentOutOfRangeException($"Invalid character index {index} not within range [0, {Count}]");
var isFirst = index == 0 && Count == 0;
var isLast = index == Count;
List<ConsoleStyleMarker> list;
if (isFirst)
{
list = _leadingStyles;
}
else if (isLast)
{
list = _trailingStyles;
}
else
{
var c = this[index];
list = c.StyleMarkers;
if (list == null)
{
list = new List<ConsoleStyleMarker>();
c.StyleMarkers = list;
this[index] = c;
}
}
list.Add(marker);
}
private void RenderLeadingTrailingStyles(TextWriter writer, bool displayStyle, bool leading, RunningStyles runningStyles)
{
var styles = leading ? _leadingStyles : _trailingStyles;
foreach (var consoleStyle in styles)
{
runningStyles.ApplyStyle(consoleStyle);
if (displayStyle)
{
runningStyles.Render(writer);
}
}
}
public void Render(ConsoleTextWriter writer, bool renderEscape = true)
{
VisibleCharacterStart = writer.VisibleCharacterCount;
if (HasSelection)
{
RenderWithSelection(writer, renderEscape);
}
else
{
var styles = new RunningStyles();
if (renderEscape) RenderLeadingTrailingStyles(writer, true, true, styles);
RenderInternal(writer, 0, Count, renderEscape, styles);
if (renderEscape) RenderLeadingTrailingStyles(writer, true, false, styles);
}
VisibleCharacterEnd = writer.VisibleCharacterCount - 1;
}
private void RenderWithSelection(ConsoleTextWriter writer, bool renderEscape = true)
{
if (writer == null) throw new ArgumentNullException(nameof(writer));
// TODO: TLS cache
var pendingStyles = renderEscape ? new RunningStyles() : null;
if (renderEscape)
{
RenderLeadingTrailingStyles(writer, true, true, pendingStyles);
}
// Display text before without selection
RenderInternal(writer, 0, SelectionStart, renderEscape, pendingStyles);
if (renderEscape)
{
// Disable any attribute sequences
Reset.Render(writer);
Reversed.Render(writer);
}
// Render the string with reverse video
RenderInternal(writer, SelectionStart, SelectionEnd, false, pendingStyles);
if (renderEscape)
{
// Disable any attribute sequences
Reset.Render(writer);
pendingStyles.Render(writer);
}
// Display text after without selection
RenderInternal(writer, SelectionEnd, this.Count, renderEscape, pendingStyles);
if (renderEscape) RenderLeadingTrailingStyles(writer, true, false, pendingStyles);
}
private void RenderInternal(ConsoleTextWriter writer, int start, int end, bool displayStyle, RunningStyles runningStyles)
{
for(int i = start; i < end; i++)
{
var c = this[i];
if ((displayStyle || runningStyles != null) && c.StyleMarkers != null)
{
foreach (var esc in c.StyleMarkers)
{
runningStyles.ApplyStyle(esc);
if (displayStyle)
{
runningStyles.Render(writer);
}
}
}
var charValue = c.Value;
// Fill the remaining line with space to clear the space
if (charValue == '\n')
{
var toComplete = Console.BufferWidth - writer.VisibleCharacterCount % Console.BufferWidth;
for (int j = 0; j < toComplete; j++)
{
writer.Write(' ');
}
writer.VisibleCharacterCount += toComplete;
}
if (charValue == '\n' || charValue >= ' ')
{
writer.Write(charValue);
}
if (charValue != '\n' || charValue >= ' ')
{
writer.VisibleCharacterCount++;
}
}
}
public static implicit operator ConsoleText(string text)
{
return new ConsoleText(text);
}
public IEnumerator<ConsoleChar> GetEnumerator()
{
return _chars.GetEnumerator();
}
public override string ToString()
{
var builder = new StringBuilder();
foreach (var c in this)
{
builder.Append(c.Value);
}
return builder.ToString();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable) _chars).GetEnumerator();
}
private class RunningStyles : Dictionary<ConsoleStyleKind, List<ConsoleStyle>>
{
public void Render(TextWriter writer)
{
// Disable any attribute sequences
Reset.Render(writer);
foreach (var stylePair in this)
{
var list = stylePair.Value;
if (stylePair.Key == ConsoleStyleKind.Color)
{
if (list.Count > 0)
{
list[list.Count - 1].Render(writer);
}
}
else if (stylePair.Key == ConsoleStyleKind.Format)
{
foreach (var item in list)
{
item.Render(writer);
}
}
}
}
public void ApplyStyle(ConsoleStyleMarker styleMarker)
{
var style = styleMarker.Style;
switch (style.Kind)
{
case ConsoleStyleKind.Color:
case ConsoleStyleKind.Format:
if (!TryGetValue(style.Kind, out var list))
{
list = new List<ConsoleStyle>();
Add(style.Kind, list);
}
if (styleMarker.Enabled)
{
list.Add(style);
}
else
{
for (var i = list.Count - 1; i >= 0; i--)
{
var item = list[i];
if (item == style)
{
list.RemoveAt(i);
break;
}
}
}
break;
case ConsoleStyleKind.Reset:
// An inline reset applies only to inline styles
if (style.IsInline)
{
// Clear only inline styles (and not regular)
foreach(var keyPair in this)
{
var styleList = keyPair.Value;
for(int i = styleList.Count - 1; i >= 0; i--)
{
var styleIn = styleList[i];
if (styleIn.IsInline)
{
styleList.RemoveAt(i);
}
}
}
}
else
{
Clear();
}
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace Consolus
{
public class ConsoleTextWriter : TextWriter
{
private char[] _buffer;
private int _count;
public ConsoleTextWriter(TextWriter writer)
{
Backend = writer ?? throw new ArgumentNullException(nameof(writer));
_buffer = new char[4096];
}
public override Encoding Encoding => Backend.Encoding;
public TextWriter Backend { get; }
public void Commit()
{
VisibleCharacterCount = 0;
Backend.Write(_buffer, 0, _count);
_count = 0;
}
public override void Write(char[] buffer, int index, int count)
{
for (int i = 0; i < count; i++)
{
Write(buffer[index + i]);
}
}
public override void Write(string value)
{
if (value == null) return;
for (int i = 0; i < value.Length; i++)
{
Write(value[i]);
}
}
public int VisibleCharacterCount { get; internal set; }
public override void Write(char value)
{
WriteInternal(value);
}
private void EnsureCapacity(int min)
{
if (_buffer.Length < min)
{
int newCapacity = _buffer.Length * 2;
// Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
if ((uint)newCapacity > int.MaxValue) newCapacity = int.MaxValue;
if (newCapacity < min) newCapacity = min;
var newBuffer = new char[newCapacity];
if (_count > 0)
{
Array.Copy(_buffer, 0,newBuffer, 0, _count);
}
_buffer = newBuffer;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteInternal(char value)
{
var newCount = _count + 1;
if (_buffer.Length < newCount)
{
EnsureCapacity(newCount);
}
_buffer[_count] = value;
_count++;
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<Target Name="_CheckForGenerateAppxPackageOnBuild" />
</Project>

View File

@@ -0,0 +1,71 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Consolus
{
internal static class WindowsHelper
{
private const int STD_OUTPUT_HANDLE = -11;
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
public static void EnableAnsiEscapeOnWindows()
{
var iStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (iStdOut != IntPtr.Zero && GetConsoleMode(iStdOut, out uint outConsoleMode))
{
SetConsoleMode(iStdOut, outConsoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
[DllImport("kernel32")]
private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);
[DllImport("kernel32")]
private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);
[DllImport("kernel32")]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32")]
private static extern IntPtr GetConsoleWindow();
[DllImport("kernel32")]
private static extern IntPtr GetCurrentProcessId();
[DllImport("user32")]
private static extern int GetWindowThreadProcessId(IntPtr hWnd, ref IntPtr ProcessId);
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool HasConsoleWindows()
{
try
{
return GetConsoleWindow() != IntPtr.Zero;
}
catch
{
return Environment.UserInteractive;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsSelfConsoleWindows()
{
try
{
IntPtr hConsole = GetConsoleWindow();
IntPtr hProcessId = IntPtr.Zero;
GetWindowThreadProcessId(hConsole, ref hProcessId);
var processId = GetCurrentProcessId();
return processId == hProcessId;
}
catch
{
return true;
}
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using CsvHelper;
using Scriban.Runtime;
namespace Kalk.Core.Helpers
{
class KalkCsvReader : IEnumerable<ScriptArray>
{
private readonly Func<TextReader> _getReader;
private readonly bool _readHeaders;
public KalkCsvReader(Func<TextReader> getReader, bool readHeaders)
{
_getReader = getReader;
_readHeaders = readHeaders;
}
public IEnumerator<ScriptArray> GetEnumerator()
{
return new CsvFileEnumerator(_getReader(), _readHeaders);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private class CsvFileEnumerator : IEnumerator<ScriptArray>
{
private readonly bool _readHeader;
private readonly TextReader _reader;
private readonly CsvReader _csvReader;
private readonly string[] _headers;
public CsvFileEnumerator(TextReader reader, bool readHeader)
{
_readHeader = readHeader;
_reader = reader;
_csvReader = new CsvReader(_reader, CultureInfo.InvariantCulture);
if (_readHeader && _csvReader.Read())
{
if (_csvReader.ReadHeader())
{
_headers = _csvReader.Context.Reader.HeaderRecord;
}
}
}
public bool MoveNext()
{
if (_csvReader.Read())
{
var array = new ScriptArray();
Current = array;
var row = _csvReader.Context.Reader.Parser.Record;
if (row != null)
{
int columnCount = row?.Length ?? 0;
if (_headers != null)
{
for (int i = 0; i < columnCount; i++)
{
var cell = ParseCell(row[i]);
array.Add(cell);
if (i < _headers.Length)
{
array.SetValue(_headers[i], cell, false);
}
}
}
else
{
for (int i = 0; i < columnCount; i++)
{
var cell = ParseCell(row[i]);
array.Add(cell);
}
}
}
return true;
}
return false;
}
private static object ParseCell(string cell)
{
if (int.TryParse(cell, out var intResult))
{
return intResult;
}
if (long.TryParse(cell, out var longResult))
{
return longResult;
}
if (BigInteger.TryParse(cell, out var bigIntResult))
{
return bigIntResult;
}
if (double.TryParse(cell, out var result))
{
return result;
}
if (DateTime.TryParse(cell, out var dateTime))
{
return dateTime;
}
return cell;
}
public void Reset()
{
throw new InvalidOperationException("Reset not supported on csv reader");
}
public ScriptArray Current { get; private set; }
object? IEnumerator.Current => Current;
public void Dispose()
{
_csvReader.Dispose();
_reader.Dispose();
}
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
namespace Kalk.Core.Helpers
{
class LineReader : IEnumerable<string>
{
private readonly Func<TextReader> _getReader;
public LineReader(Func<TextReader> getReader)
{
this._getReader = getReader;
}
public IEnumerator<string> GetEnumerator()
{
return new LineEnumerator(_getReader());
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private class LineEnumerator : IEnumerator<string>
{
private readonly TextReader _reader;
public LineEnumerator(TextReader reader)
{
_reader = reader;
}
public bool MoveNext()
{
Current = _reader.ReadLine();
return Current != null;
}
public void Reset()
{
throw new NotSupportedException("Cannot reset a line reader");
}
public string Current { get; private set; }
object? IEnumerator.Current => Current;
public void Dispose()
{
_reader.Dispose();
}
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Scriban.Runtime;
namespace Kalk.Core.Helpers
{
internal static class UnsafeHelpers
{
public static unsafe object BitCast<T>(KalkNativeBuffer src) where T: unmanaged
{
var countItem = src.Count / sizeof(T);
if (countItem == 0)
{
return default(T);
}
if (countItem == 1)
{
return MemoryMarshal.Cast<byte, T>(src.AsSpan())[0];
}
else
{
var array = MemoryMarshal.Cast<byte, T>(src.AsSpan());
var scriptArray = new ScriptArray(array.Length);
for (int i = 0; i < array.Length; i++)
{
scriptArray[i] = array[i];
}
return scriptArray;
}
}
}
}

View File

@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.IO;
namespace Kalk.Core
{
/// <summary>
/// Simple file abstraction to mitigate UWP access issues.
/// </summary>
public interface IFileService
{
Stream FileOpen(string path, FileMode mode, FileAccess access, FileShare share);
void DirectoryCreate(string path);
IEnumerable<string> EnumerateFileSystemEntries(
string path,
string searchPattern,
SearchOption searchOption);
void DirectoryDelete(string path, bool recursive);
string FileReadAllText(string path);
void FileDelete(string path);
bool FileExists(string path);
bool DirectoryExists(string path);
}
/// <summary>
/// Default implementation of file service using .NET API directly (not working on UWP)
/// </summary>
class DefaultFileService : IFileService
{
public Stream FileOpen(string path, FileMode mode, FileAccess access, FileShare share)
{
return new FileStream(path, mode, access, share);
}
public void DirectoryCreate(string path)
{
Directory.CreateDirectory(path);
}
public IEnumerable<string> EnumerateFileSystemEntries(string path, string searchPattern, SearchOption searchOption)
{
return Directory.EnumerateFileSystemEntries(path, searchPattern, searchOption);
}
public void DirectoryDelete(string path, bool recursive)
{
Directory.Delete(path, recursive);
}
public string FileReadAllText(string path)
{
return File.ReadAllText(path);
}
public void FileDelete(string path)
{
File.Delete(path);
}
public bool FileExists(string path)
{
return File.Exists(path);
}
public bool DirectoryExists(string path)
{
return Directory.Exists(path);
}
}
}

View File

@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<KalkGitVersion>true</KalkGitVersion>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS8632</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Remove="core.kalk" />
<None Remove="units.kalk" />
<EmbeddedResource Include="units.kalk" />
<EmbeddedResource Include="core.kalk" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="System.Runtime.Numerics" Version="4.3.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
<PackageReference Include="Scriban" Version="5.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Consolus\Consolus.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Modules\Vectors\colorspaces.kalk">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Modules\Vectors\TypesModule.tt">
<Generator>TextTemplatingFileGenerator</Generator>
<LastGenOutput>TypesModule.generated.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Compile Update="Modules\Vectors\TypesModule.generated.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>TypesModule.tt</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=types/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kalk.Core", "Kalk.Core.csproj", "{0CB7389A-95B7-4CFF-B1E4-29BCD14B05B6}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Consolus", "..\Consolus\Consolus.csproj", "{C937AD35-3CAC-4EF7-89E0-D70220FF320E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0CB7389A-95B7-4CFF-B1E4-29BCD14B05B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0CB7389A-95B7-4CFF-B1E4-29BCD14B05B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CB7389A-95B7-4CFF-B1E4-29BCD14B05B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CB7389A-95B7-4CFF-B1E4-29BCD14B05B6}.Release|Any CPU.Build.0 = Release|Any CPU
{C937AD35-3CAC-4EF7-89E0-D70220FF320E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C937AD35-3CAC-4EF7-89E0-D70220FF320E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C937AD35-3CAC-4EF7-89E0-D70220FF320E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C937AD35-3CAC-4EF7-89E0-D70220FF320E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8D4CA131-59E2-4B7B-B8CA-97587622D4D0}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,30 @@
namespace Kalk.Core
{
public enum KalkAction
{
Exit,
CursorLeft,
CursorRight,
CursorLeftWord,
CursorRightWord,
CursorStartOfLine,
CursorEndOfLine,
HistoryPrevious,
HistoryNext,
DeleteCharacterLeft,
DeleteCharacterLeftAndCopy,
DeleteCharacterRight,
DeleteCharacterRightAndCopy,
DeleteWordLeft,
DeleteWordRight,
Completion,
DeleteTextRightAndCopy,
DeleteWordRightAndCopy,
DeleteWordLeftAndCopy,
CopySelection,
CutSelection,
PasteClipboard,
ValidateLine,
ForceValidateLine,
}
}

View File

@@ -0,0 +1,90 @@
using System;
using Scriban.Runtime;
namespace Kalk.Core
{
/// <summary>
/// An action object returned by <see cref="KalkEngine.Action"/> and used when evaluating shortcuts.
/// </summary>
public class KalkActionObject : ScriptObject
{
public KalkActionObject(string action)
{
Action = action ?? throw new ArgumentNullException(nameof(action));
}
public string Action
{
get => GetSafeValue<string>("action");
set => SetValue("action", value, false);
}
public void Call(Action<KalkAction> run)
{
var action = Action;
if (action == null) return;
switch (action)
{
case "cursor_left":
run(KalkAction.CursorLeft);
break;
case "cursor_right":
run(KalkAction.CursorRight);
break;
case "history_previous":
run(KalkAction.HistoryPrevious);
break;
case "history_next":
run(KalkAction.HistoryNext);
break;
case "copy":
run(KalkAction.CopySelection);
break;
case "cut":
run(KalkAction.CutSelection);
break;
case "paste":
run(KalkAction.PasteClipboard);
break;
case "cursor_word_left":
run(KalkAction.CursorLeftWord);
break;
case "cursor_word_right":
run(KalkAction.CursorRightWord);
break;
case "cursor_line_start":
run(KalkAction.CursorStartOfLine);
break;
case "cursor_line_end":
run(KalkAction.CursorEndOfLine);
break;
case "completion":
run(KalkAction.Completion);
break;
case "delete_left":
run(KalkAction.DeleteCharacterLeft);
break;
case "delete_right":
run(KalkAction.DeleteCharacterRight);
break;
case "delete_word_left":
run(KalkAction.DeleteWordLeft);
break;
case "delete_word_right":
run(KalkAction.DeleteWordRight);
break;
case "validate_line":
run(KalkAction.ValidateLine);
break;
case "force_validate_line":
run(KalkAction.ForceValidateLine);
break;
case "exit":
run(KalkAction.Exit);
break;
default:
throw new ArgumentException($"Action `{action}` is not supported", nameof(action));
}
}
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkAliases : ScriptObject, IScriptCustomFunction
{
public KalkAliases()
{
Aliases = new Dictionary<string, string>();
}
public Dictionary<string, string> Aliases { get; }
public int RequiredParameterCount => 0;
public int ParameterCount => 0;
public ScriptVarParamKind VarParamKind => ScriptVarParamKind.None;
public Type ReturnType => typeof(object);
public ScriptParameterInfo GetParameterInfo(int index)
{
throw new NotSupportedException("Aliases don't have any parameters.");
}
public void AddAlias(KalkAlias alias)
{
if (alias == null) throw new ArgumentNullException(nameof(alias));
Add(alias.Name, alias);
foreach (string aliasName in alias.Aliases)
{
Aliases[aliasName] = alias.Name;
}
}
public bool TryGetAlias(string name, out string alias)
{
return Aliases.TryGetValue(name, out alias);
}
public bool TryGetValue(string key, out KalkAlias value)
{
value = null;
if (TryGetValue(null, new SourceSpan(), key, out var valueObj))
{
value = (KalkAlias) valueObj;
return true;
}
return false;
}
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
{
// In the case of using KalkSymbols outside of the scripting engine
if (context == null) return base.TrySetValue(null, span, member, value, readOnly);
// Otherwise, we are not allowing to modify this object.
throw new ScriptRuntimeException(span, "Aliases object can't be modified directly. You need to use the command `shortcut` instead.");
}
public object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
Display((KalkEngine)context, "Builtin Aliases", filter => !filter.IsUser);
Display((KalkEngine) context, "User-defined Aliases", filter => filter.IsUser);
return null;
}
public void Display(KalkEngine engine, string title, Func<KalkAlias, bool> filter = null, bool addBlankLine = false)
{
if (engine == null) throw new ArgumentNullException(nameof(engine));
if (title == null) throw new ArgumentNullException(nameof(title));
bool isFirst = true;
foreach (var aliasKey in this.Keys.OrderBy(x => x))
{
var alias = this[aliasKey] as KalkAlias;
if (alias == null || (filter != null && !filter(alias))) continue;
if (isFirst)
{
engine.WriteHighlightLine($"# {title}");
}
else if (addBlankLine)
{
engine.WriteHighlightLine("");
}
isFirst = false;
engine.WriteHighlightLine(alias.ToString());
}
}
public ValueTask<object> InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(Invoke(context, callerContext, arguments, blockStatement));
}
}
}

View File

@@ -0,0 +1,43 @@
using Scriban.Runtime;
namespace Kalk.Core
{
public class KalkConfig : ScriptObject
{
public const int DefaultHelpMinColumn = 30;
public const int DefaultHelpMaxColumn = 100;
public const string DefaultLimitToString = "auto";
public const string DefaultEncoding = "utf-8";
public const string LimitToStringProp = "limit_to_string";
public const string EncodingProp = "encoding";
public KalkConfig()
{
HelpMaxColumn = DefaultHelpMaxColumn;
LimitToString = DefaultLimitToString;
}
public int HelpMaxColumn
{
get => GetSafeValue<int>("help_max_column", DefaultHelpMaxColumn);
set
{
if (value < DefaultHelpMinColumn) value = DefaultHelpMinColumn;
SetValue("help_max_column", value, false);
}
}
public string LimitToString
{
get => GetSafeValue(LimitToStringProp, DefaultLimitToString);
set => SetValue(LimitToStringProp, value, false);
}
public string Encoding
{
get => GetSafeValue(EncodingProp, DefaultEncoding);
set => SetValue(EncodingProp, value, false);
}
}
}

View File

@@ -0,0 +1,11 @@
namespace Kalk.Core
{
public enum KalkDisplayMode
{
Raw,
Standard,
Developer
}
}

View File

@@ -0,0 +1,52 @@
using System;
namespace Kalk.Core
{
public static class KalkDisplayModeHelper
{
public const string Raw = "raw";
public const string Standard = "std";
public const string Developer = "dev";
public static string ToText(this KalkDisplayMode mode)
{
switch (mode)
{
case KalkDisplayMode.Raw:
return Raw;
case KalkDisplayMode.Standard:
return Standard;
case KalkDisplayMode.Developer:
return Developer;
default:
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
}
}
public static bool TryParse(string mode, out KalkDisplayMode fullMode)
{
fullMode = KalkDisplayMode.Standard;
switch (mode)
{
case Raw:
fullMode = KalkDisplayMode.Raw;
return true;
case Standard:
fullMode = KalkDisplayMode.Standard;
return true;
case Developer:
fullMode = KalkDisplayMode.Developer;
return true;
}
return false;
}
public static KalkDisplayMode SafeParse(string text)
{
return TryParse(text, out var mode) ? mode : KalkDisplayMode.Standard;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,270 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine
{
public const string CategoryUnits = "Unit Functions";
/// <summary>
/// If used in an expression, returns an object containing all units defined.
/// Otherwise it will display units in a friendly format.
/// By default, no units are defined. You can define units by using the `unit` function
/// and you can also import predefined units or currencies via `import StandardUnits` or
/// `import Currencies`.
/// </summary>
/// <example>
/// ```kalk
/// >>> unit(tomato, "A tomato unit", prefix: "decimal")
/// # unit(tomato, "A tomato unit", prefix: "decimal")
/// out = tomato
/// >>> unit(ketchup, "A ketchup unit", kup, 5 tomato, prefix: "decimal")
/// # unit(ketchup, "A ketchup unit", kup, 5 * tomato, prefix: "decimal")
/// out = kup
/// >>> units
/// # User Defined Units
/// unit(ketchup, "A ketchup unit", kup, 5 * tomato, prefix: "decimal")
/// - yottaketchup/Ykup, zettaketchup/Zkup, exaketchup/Ekup, petaketchup/Pkup, teraketchup/Tkup,
/// gigaketchup/Gkup, megaketchup/Mkup, kiloketchup/kkup, hectoketchup/hkup, decaketchup/dakup,
/// deciketchup/dkup, centiketchup/ckup, milliketchup/mkup, microketchup/µkup, nanoketchup/nkup,
/// picoketchup/pkup, femtoketchup/fkup, attoketchup/akup, zeptoketchup/zkup, yoctoketchup/ykup
///
/// unit(tomato, "A tomato unit", tomato, prefix: "decimal")
/// - yottatomato/Ytomato, zettatomato/Ztomato, exatomato/Etomato, petatomato/Ptomato,
/// teratomato/Ttomato, gigatomato/Gtomato, megatomato/Mtomato, kilotomato/ktomato,
/// hectotomato/htomato, decatomato/datomato, decitomato/dtomato, centitomato/ctomato,
/// millitomato/mtomato, microtomato/µtomato, nanotomato/ntomato, picotomato/ptomato,
/// femtotomato/ftomato, attotomato/atomato, zeptotomato/ztomato, yoctotomato/ytomato
/// ```
/// </example>
[KalkExport("units", CategoryUnits)]
public KalkUnits Units { get; }
/// <summary>
/// Converts from one value unit to a destination unit.
///
/// The pipe operator |> can be used between the src and destination unit to make it
/// more readable. Example: `105 g |> to kg`
/// </summary>
/// <param name="src">The source value with units.</param>
/// <param name="dst">The destination unit.</param>
/// <returns>The result of the calculation.</returns>
/// <example>
/// ```kalk
/// >>> import StandardUnits
/// # 1294 units successfully imported from module `StandardUnits`.
/// >>> 10 kg/s |> to kg/h
/// # ((10 * kg) / s) |> to(kg / h)
/// out = 36000 * kg / h
/// >>> 50 kg/m |> to g/km
/// # ((50 * kg) / m) |> to(g / km)
/// out = 50000000 * g / km
/// ```
/// </example>
[KalkExport("to", CategoryUnits)]
public KalkExpression ConvertTo(KalkExpression src, KalkExpression dst)
{
return src.ConvertTo(this, dst);
}
/// <summary>
/// Defines a unit with the specified name and characteristics.
/// </summary>
/// <param name="name">Long name of the unit.</param>
/// <param name="description">A description of the unit. This value is optional.</param>
/// <param name="symbol">Short name (symbol) of the unit. This value is optional.</param>
/// <param name="value">The expression value of this unit. This value is optional.</param>
/// <param name="plural">The plural name of this unit. This value is optional.</param>
/// <param name="prefix">A comma list separated of prefix kinds:
/// - "decimal": Defines the twenty prefixes for the International System of Units (SI). Example: Yotta/Y, kilo/k, milli/m...
/// - "binary": Defines the binary prefixes. See https://en.wikipedia.org/wiki/Binary_prefix. Example: kibbi/Ki, mebi/Mi...
/// - Individual prefixes:
/// Decimal prefixes:
/// - `Y` - `Yotta` (10^24)
/// - `Z` - `Zetta` (10^21)
/// - `E` - `Exa` (10^18)
/// - `P` - `Peta` (10^15)
/// - `T` - `Tera` (10^12)
/// - `G` - `Giga` (10^9)
/// - `M` - `Mega` (10^6)
/// - `k` - `kilo` (10^3)
/// - `h` - `hecto` (10^2)
/// - `da` - `deca` (10^1)
/// - `d` - `deci` (10^)-1
/// - `c` - `centi` (10^)-2
/// - `m` - `milli` (10^)-3
/// - `µ` - `micro` (10^-6)
/// - `n` - `nano` (10^)-9
/// - `p` - `pico` (10^)-12
/// - `f` - `femto` (10^)-15
/// - `a` - `atto` (10^)-18
/// - `z` - `zepto` (10^)-21
/// - `y` - `yocto` (10^)-24
///
/// Binary prefixes:
/// - `Ki` - `Kibi` (2^10)
/// - `Mi` - `Mibi` (2^20)
/// - `Gi` - `Gibi` (2^30)
/// - `Ti` - `Tibi` (2^40)
/// - `Pi` - `Pibi` (2^50)
/// - `Ei` - `Eibi` (2^60)
/// - `Zi` - `Zibi` (2^70)
/// - `Yi` - `Yibi` (2^80)
/// </param>
/// <returns>The associated unit object.</returns>
/// <example>
/// ```kalk
/// >>> unit(tomato, "A tomato unit", prefix: "decimal")
/// # unit(tomato, "A tomato unit", prefix: "decimal")
/// out = tomato
/// >>> unit(ketchup, "A ketchup unit", kup, 5 tomato, prefix: "decimal")
/// # unit(ketchup, "A ketchup unit", kup, 5 * tomato, prefix: "decimal")
/// out = kup
/// >>> 4 kup
/// # 4 * kup
/// out = 20 * tomato
/// >>> tomato
/// unit(tomato, "A tomato unit", tomato, prefix: "decimal")
/// - yottatomato/Ytomato, zettatomato/Ztomato, exatomato/Etomato, petatomato/Ptomato,
/// teratomato/Ttomato, gigatomato/Gtomato, megatomato/Mtomato, kilotomato/ktomato,
/// hectotomato/htomato, decatomato/datomato, decitomato/dtomato, centitomato/ctomato,
/// millitomato/mtomato, microtomato/µtomato, nanotomato/ntomato, picotomato/ptomato,
/// femtotomato/ftomato, attotomato/atomato, zeptotomato/ztomato, yoctotomato/ytomato
/// >>> ketchup
/// unit(ketchup, "A ketchup unit", kup, 5 * tomato, prefix: "decimal")
/// - yottaketchup/Ykup, zettaketchup/Zkup, exaketchup/Ekup, petaketchup/Pkup, teraketchup/Tkup,
/// gigaketchup/Gkup, megaketchup/Mkup, kiloketchup/kkup, hectoketchup/hkup, decaketchup/dakup,
/// deciketchup/dkup, centiketchup/ckup, milliketchup/mkup, microketchup/µkup, nanoketchup/nkup,
/// picoketchup/pkup, femtoketchup/fkup, attoketchup/akup, zeptoketchup/zkup, yoctoketchup/ykup
/// ```
/// </example>
///
[KalkExport("unit", CategoryUnits)]
public KalkExpression DefineUserUnit(ScriptVariable name, string description = null, ScriptVariable symbol = null, KalkExpression value = null, string plural = null, string prefix = null)
{
if (name == null || string.IsNullOrEmpty(name.Name)) throw new ArgumentNullException(nameof(name));
return RegisterUnit(new KalkUnit(name.Name), description, symbol?.Name, value, plural, prefix, isUser: true);
}
public KalkExpression RegisterUnit(KalkUnit unit, string description = null, string symbol = null, KalkExpression value = null, string plural = null, string prefix = null, bool isUser = false)
{
if (unit == null) throw new ArgumentNullException(nameof(unit));
var name = unit.Name;
symbol ??= name;
// Override isUser
if (_registerAsSystem) isUser = false;
CheckVariableAvailable(name, nameof(name), false);
var prefixList = new List<KalkUnitPrefix>();
if (prefix != null)
{
var prefixes = prefix.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
foreach(var prefixItem in prefixes)
{
if (prefixItem == "decimal")
{
prefixList.AddRange(KalkUnitPrefix.GetDecimals());
}
else if (prefixItem == "binary")
{
prefixList.AddRange(KalkUnitPrefix.GetBinaries());
}
else if (KalkUnitPrefix.TryGet(prefixItem, out var descriptor))
{
prefixList.Add(descriptor);
}
else
{
throw new ArgumentException($"The prefix `{prefixItem}` does not exist.", nameof(prefix));
}
}
prefixList = prefixList.Distinct().ToList();
}
// Pre-check all prefix with name/symbol
foreach (var prefixDesc in prefixList)
{
var prefixWithName = $"{prefixDesc.Name}{name}";
CheckVariableAvailable(prefixWithName, nameof(name), false);
var prefixWithSymbol = $"{prefixDesc.Prefix}{symbol}";
CheckVariableAvailable(prefixWithSymbol, nameof(name), false);
}
unit.Description = description;
unit.Symbol = symbol;
unit.Value = value;
unit.IsUser = isUser;
unit.Prefix = prefix;
if (plural != null)
{
unit.Plural = plural;
}
if (unit.Symbol != unit.Name)
{
CheckVariableAvailable(unit.Symbol, nameof(symbol), false);
}
if (unit.Plural != unit.Name)
{
CheckVariableAvailable(unit.Plural, nameof(plural), false);
}
// Here we are all done after checking everything
Units.Add(name, unit);
if (unit.Symbol != unit.Name)
{
Units.Add(unit.Symbol, unit);
}
if (unit.Plural != unit.Name)
{
Units.Add(unit.Plural, unit);
}
// Register prefixes
foreach (var prefixDesc in prefixList)
{
var prefixWithName = $"{prefixDesc.Name}{unit.Name}";
var prefixWithSymbol = $"{prefixDesc.Prefix}{unit.Symbol}";
var unitPrefix = new KalkUnit(prefixWithName)
{
Description = description,
Symbol = prefixWithSymbol,
Value = new KalkBinaryExpression(Math.Pow(prefixDesc.Base, prefixDesc.Exponent), ScriptBinaryOperator.Multiply, unit),
IsUser = isUser,
Parent = unit,
};
unit.Derived.Add(unitPrefix);
Units.Add(prefixWithName, unitPrefix);
Units.Add(prefixWithSymbol, unitPrefix);
}
return unit;
}
private void CheckVariableAvailable(string name, string nameOf, bool prefix)
{
if (Units.ContainsKey(name))
{
throw new ArgumentException(prefix ? $"The name with prefix `{name}` is already used by another unit." : $"The name `{name}` is already used by another unit.", nameOf);
}
if (Builtins.ContainsKey(name))
{
throw new ArgumentException(prefix ? $"The name with prefix `{name}` is already used a builtin variable or function." : $"The name `{name}` is already used a builtin variable or function.", nameOf);
}
}
}
}

View File

@@ -0,0 +1,312 @@
using System;
using System.IO;
using Kalk.Core.Modules;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine
{
private bool _registerAsSystem;
private const string CoreFileName = "core.kalk";
private MiscModule _miscModule;
private void RegisterFunctions()
{
RegisterFunctionsAuto();
// builtins
GetOrCreateModule<MathModule>();
_miscModule = GetOrCreateModule<MiscModule>();
GetOrCreateModule<MemoryModule>();
GetOrCreateModule<TypesModule>();
GetOrCreateModule<VectorModule>();
var allModule = GetOrCreateModule<AllModule>();
allModule.Modules.Add(GetOrCreateModule<HardwareIntrinsicsModule>());
FileModule = GetOrCreateModule<FileModule>();
allModule.Modules.Add(FileModule);
allModule.Modules.Add(GetOrCreateModule<CsvModule>());
allModule.Modules.Add(GetOrCreateModule<StringModule>());
allModule.Modules.Add(GetOrCreateModule<CurrencyModule>());
allModule.Modules.Add(GetOrCreateModule<StandardUnitsModule>());
allModule.Modules.Add(GetOrCreateModule<WebModule>());
// Register last the system file
// shortcut(exit, "ctrl+z", action("exit"))
// Setup the exit depending on OS
//EvaluateTextImpl($"shortcut(exit, '{(OperatingSystem.IsWindows() ? "ctrl+z" : "ctrl+d")}', action('exit'))", string.Empty);
LoadCoreFile();
}
public FileModule FileModule { get; private set; }
private void LoadCoreFile()
{
LoadSystemFileFromResource(CoreFileName);
}
private void LoadUserConfigFile()
{
if (KalkUserFolder == null) return;
const string configFileName = "config.kalk";
var userConfigFile = Path.Combine(KalkUserFolder, configFileName);
//WriteHighlightLine($"# Try loading from {userConfigFile}");
if (!FileService.FileExists(userConfigFile)) return;
using var stream = FileService.FileOpen(userConfigFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
LoadConfigFile($".kalk/{configFileName}", stream, false);
}
internal void LoadSystemFileFromResource(string filename)
{
var stream = typeof(KalkEngine).Assembly.GetManifestResourceStream($"{typeof(KalkEngine).Namespace}.{filename}");
if (stream == null)
{
throw new FileNotFoundException($"The resource {filename} was not found");
}
LoadConfigFile(filename, stream, true);
}
internal void LoadConfigFile(string filename, Stream stream, bool isSystem)
{
// Register all units
bool previousEchoEnabled = EchoEnabled;
try
{
_registerAsSystem = isSystem;
EchoEnabled = false;
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
EvaluateTextImpl(text, filename, false);
}
catch (Exception ex)
{
WriteErrorLine($"Unable to load file {filename}. Reason:\n{ex.Message}");
}
finally
{
EchoEnabled = previousEchoEnabled;
_registerAsSystem = false;
}
}
protected void RegisterAction(string name, Action action, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1>(string name, Action<T1> action, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2>(string name, Action<T1, T2> action, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2, T3>(string name, Action<T1, T2, T3> action, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2, T3, T4>(string name, Action<T1, T2, T3, T4> action, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.Create(action), category);
}
public void RegisterConstant(string name, object value, string category = null)
{
RegisterVariable(name, value, category);
}
public void RegisterFunction(string name, Delegate del, string category = null)
{
RegisterFunction(name, new DelegateCustomFunction(del), category);
}
public void RegisterFunction(string name, Func<KalkMatrix, object> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<KalkMatrix, KalkMatrix> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<KalkVector, KalkVector, object> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, object, object> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, bool> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<double, double> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, double> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, long> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<double, double, double> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<KalkDoubleValue, object> func, string category = null)
{
RegisterFunction(name, Wrap(func), category);
}
public void RegisterFunction(string name, Func<KalkLongValue, object> func, string category = null)
{
RegisterFunction(name, Wrap(func), category);
}
public void RegisterFunction(string name, Func<KalkIntValue, object> func, string category = null)
{
RegisterFunction(name, Wrap(func), category);
}
public void RegisterFunction(string name, Func<KalkCompositeValue, object> func, string category = null)
{
RegisterFunction(name, Wrap(func), category);
}
public void RegisterFunction(string name, Func<object, int> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkVector<int>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkVector<KalkBool>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkVector<float>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkVector<double>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkMatrix<KalkBool>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkMatrix<int>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkMatrix<double>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkMatrix<float>> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkColorRgb> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object[], KalkColorRgba> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<ScriptVariable, int, object[], object> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, float> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, Func<object, object> func, string category = null)
{
RegisterFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
public void RegisterFunction(string name, IScriptCustomFunction func, string category = null)
{
RegisterVariable(name, func, category);
}
private void RegisterVariable(string name, object value, string category = null)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (value == null) throw new ArgumentNullException(nameof(value));
var names = name.Split(',');
KalkDescriptor descriptor = null;
foreach (var subName in names)
{
Builtins.SetValue(subName, value, true);
if (descriptor == null || !Descriptors.TryGetValue(names[0], out descriptor))
{
descriptor = new KalkDescriptor();
}
Descriptors.Add(subName, descriptor);
descriptor.Names.Add(subName);
}
}
private static DelegateCustomFunction Wrap(Func<KalkDoubleValue, object> func)
{
return DelegateCustomFunction.CreateFunc(func);
}
private static DelegateCustomFunction Wrap(Func<KalkIntValue, object> func)
{
return DelegateCustomFunction.CreateFunc(func);
}
private static DelegateCustomFunction Wrap(Func<KalkCompositeValue, object> func)
{
return DelegateCustomFunction.CreateFunc(func);
}
private static DelegateCustomFunction Wrap(Func<KalkLongValue, object> func)
{
return DelegateCustomFunction.CreateFunc(func);
}
}
}

View File

@@ -0,0 +1,815 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Consolus;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine
{
public const int DefaultLimitToStringNoAuto = 4096;
private static readonly TokenType[] MatchPairs = new[]
{
TokenType.OpenParen,
TokenType.CloseParen,
TokenType.OpenBracket,
TokenType.CloseBracket,
TokenType.OpenBrace,
TokenType.CloseBrace,
};
private readonly HashSet<string> ScriptKeywords = new HashSet<string>()
{
"if",
"else",
"end",
"for",
"in",
"case",
"when",
"while",
"break",
"continue",
"func",
"import",
"readonly",
"with",
"capture",
"ret",
"wrap",
"do",
};
private readonly HashSet<string> ValueKeywords = new HashSet<string>()
{
"null",
"true",
"false",
};
private static List<string> SplitStringBySpaceAndKeepSpace(string text)
{
// normalize line endings with \n
text = text.Replace("\r\n", "\n").Replace("\r", "\n");
var list = new List<string>();
var builder = new StringBuilder();
bool isCurrentWhiteSpace = false;
for (int i = 0; i < text.Length; i++)
{
var c = text[i];
if (char.IsWhiteSpace(c))
{
if (builder.Length > 0)
{
list.Add(builder.ToString());
builder.Length = 0;
}
// We put "\n" separately
if (c == '\n')
{
list.Add("\n");
continue;
}
isCurrentWhiteSpace = true;
}
else if (isCurrentWhiteSpace)
{
list.Add(builder.ToString());
builder.Length = 0;
isCurrentWhiteSpace = false;
}
builder.Append(c);
}
if (builder.Length > 0)
{
list.Add(builder.ToString());
}
return list;
}
private void WriteHighlightVariableAndValueToConsole(string name, object value)
{
if (value is ScriptFunction function && !function.IsAnonymous)
{
WriteHighlightLine($"{value}");
}
else
{
int previousLimit = LimitToString;
try
{
int limit = 0;
var limitStr = Config.LimitToString;
if (limitStr == "auto")
{
if (HasInteractiveConsole && IsOutputSupportHighlighting)
{
limit = Console.WindowWidth * Console.WindowHeight;
}
else
{
limit = DefaultLimitToStringNoAuto;
}
}
else if (limitStr == null || !int.TryParse(limitStr, out limit))
{
limit = DefaultLimitToStringNoAuto;
}
if (limit < 0) limit = 0;
LimitToString = limit;
WriteHighlightLine($"{name} = {ObjectToString(value, true)}");
}
finally
{
LimitToString = previousLimit;
}
WriteValueWithDisplayMode(value);
}
}
private void WriteValueWithDisplayMode(object value)
{
// If the type is displayable
if (value is IKalkDisplayable displayable)
{
displayable.Display(this, CurrentDisplay);
return;
}
// Otherwise supports the default.
switch (CurrentDisplay)
{
case KalkDisplayMode.Raw:
case KalkDisplayMode.Standard:
return;
case KalkDisplayMode.Developer:
if (value is int i32)
{
WriteInt32(i32);
}
else if (value is uint u32)
{
WriteUInt32(u32);
}
else if (value is short i16)
{
WriteInt16(i16);
}
else if (value is ushort u16)
{
WriteUInt16(u16);
}
else if (value is sbyte i8)
{
WriteInt8(i8);
}
else if (value is byte u8)
{
WriteUInt8(u8);
}
else if (value is long i64)
{
WriteInt64(i64);
}
else if (value is ulong u64)
{
WriteUInt64(u64);
}
else if (value is float f32)
{
WriteFloat(f32);
}
else if (value is double f64)
{
WriteDouble(f64, "IEEE 754 - double - 64-bit");
}
else if (value is decimal dec)
{
WriteDouble((double)dec, "Decimal 128-bit displayed as IEEE 754 - double - 64-bit");
}
return;
}
}
private void WriteInt32(int i32)
{
WriteHighlightLine($" # int - 32-bit");
WriteInt32((uint)i32, false);
}
private void WriteInt16(short i16)
{
WriteHighlightLine($" # short - 16-bit");
WriteInt16((ushort)i16, false);
}
private void WriteUInt16(ushort u16)
{
WriteHighlightLine($" # ushort - 16-bit");
WriteInt16(u16, false);
}
private void WriteUInt32(uint u32)
{
WriteHighlightLine($" # uint - 32-bit");
WriteInt32(u32, false);
}
private void WriteUInt8(byte u8)
{
WriteHighlightLine($" # byte - 8-bit");
WriteInt8Raw(u8);
}
private void WriteInt8(sbyte u8)
{
WriteHighlightLine($" # sbyte - 8-bit");
WriteInt8Raw((byte)u8);
}
private void WriteInt32(uint u32, bool forFloat)
{
WriteHighlightLine($" = 0x_{u32 >> 16:X4}_{u32 & 0xFFFF:X4}");
var builder = new StringBuilder();
// Prints the hexa version
// # 0x____3____F____F____8____0____0____0____0
builder.Append("0x");
for (int i = 7; i >= 0; i--)
{
builder.Append("____");
var v = (byte)(0xF & (u32 >> (i * 4)));
builder.Append(v.ToString("X1"));
}
WriteHighlightLine($" = {builder}");
if (forFloat)
{
// Prints the float IEE754 mask
// # seee eeee efff ffff ffff ffff ffff ffff
WriteHighlightLine($" # seee eeee efff ffff ffff ffff ffff ffff");
}
// Prints the binary version
// # 0b_0011_1111_1111_1000_0000_0000_0000_0000
builder.Length = 0;
builder.Append("0b");
for (int i = 7; i >= 0; i--)
{
builder.Append('_');
var v = (byte)(0xF & (u32 >> (i * 4)));
var leadingZero = BitOperations.LeadingZeroCount(v) - 28;
builder.Append('0', leadingZero);
if (v != 0) builder.Append(Convert.ToString(v, 2));
}
WriteHighlightLine($" = {builder}");
}
internal void WriteInt16(uint u16, bool forHalf)
{
WriteHighlightLine($" = 0x_{u16:X4}");
var builder = new StringBuilder();
// Prints the hexa version
// # 0x____3____F____F____8
builder.Append("0x");
for (int i = 3; i >= 0; i--)
{
builder.Append("____");
var v = (byte)(0xF & (u16 >> (i * 4)));
builder.Append(v.ToString("X1"));
}
WriteHighlightLine($" = {builder}");
if (forHalf)
{
// Prints the float IEE754 mask
// # seee eeff ffff ffff
WriteHighlightLine($" # seee eeff ffff ffff");
}
// Prints the binary version
// # 0b_0011_1111_1111_1000
builder.Length = 0;
builder.Append("0b");
for (int i = 3; i >= 0; i--)
{
builder.Append('_');
var v = (byte)(0xF & (u16 >> (i * 4)));
var leadingZero = BitOperations.LeadingZeroCount(v) - 28;
builder.Append('0', leadingZero);
if (v != 0) builder.Append(Convert.ToString(v, 2));
}
WriteHighlightLine($" = {builder}");
}
internal void WriteInt8Raw(byte u8)
{
WriteHighlightLine($" = 0x_{u8:X2}");
var builder = new StringBuilder();
// Prints the hexa version
// # 0x____3____F
builder.Append("0x");
for (int i = 1; i >= 0; i--)
{
builder.Append("____");
var v = (byte)(0xF & (u8 >> (i * 4)));
builder.Append(v.ToString("X1"));
}
WriteHighlightLine($" = {builder}");
// Prints the binary version
// # 0b_0011_1111
builder.Length = 0;
builder.Append("0b");
for (int i = 1; i >= 0; i--)
{
builder.Append('_');
var v = (byte)(0xF & (u8 >> (i * 4)));
var leadingZero = BitOperations.LeadingZeroCount(v) - 28;
builder.Append('0', leadingZero);
if (v != 0) builder.Append(Convert.ToString(v, 2));
}
WriteHighlightLine($" = {builder}");
}
private void WriteInt64(long i64)
{
WriteHighlightLine($" # long - 64-bit");
WriteUInt64((ulong)i64, false);
}
private void WriteUInt64(ulong u64)
{
WriteHighlightLine($" # ulong - 64-bit");
WriteUInt64(u64, false);
}
private void WriteUInt64(ulong u64, bool forDouble)
{
WriteHighlightLine($" = 0x_{u64 >> 32:X8}_{((uint)u64) & 0xFFFFFFFF:X8}");
var builder = new StringBuilder();
// Prints the hexa version
// # 0x____3____F____F____8____0____0____0____0____0____0____0____0____0____0____0____0
builder.Append("0x");
for (int i = 15; i >= 0; i--)
{
builder.Append("____");
var v = (byte)(0xF & (u64 >> (i * 4)));
builder.Append(v.ToString("X1"));
}
WriteHighlightLine($" = {builder}");
if (forDouble)
{
// Prints the double IEE754 mask
// # seee eeee eeee ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff
WriteHighlightLine($" # seee eeee eeee ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff ffff");
}
// Prints the binary version
// # 0b_0011_1111_1111_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000
builder.Length = 0;
builder.Append("0b");
for (int i = 15; i >= 0; i--)
{
builder.Append('_');
var v = (byte)(0xF & (u64 >> (i * 4)));
var leadingZero = BitOperations.LeadingZeroCount(v) - 28;
builder.Append('0', leadingZero);
if (v != 0) builder.Append(Convert.ToString(v, 2));
}
WriteHighlightLine($" = {builder}");
}
private void WriteDouble(double f64, string title)
{
var u64 = (ulong)BitConverter.DoubleToInt64Bits(f64);
WriteHighlightLine($" # {title}");
WriteHighlightLine($" #");
WriteUInt64(u64, true);
// Prints the 16 bits indices
WriteHighlightLine($" # 63 48 32 16 0");
// Prints the binary version
// # s eeee ffffffffffffffffffffffffffffffffffffffffffffffffffff
WriteHighlightLine($" #");
WriteHighlightLine($" # sign exponent |-------------------- fraction --------------------|");
// # 1 * 2 ^ (1023 - 1023) * 0b1.1000000000000000000000000000000000000000000000000000
var sign = (u64 >> 63);
var exponent = (u64 >> 52) & 0b_111_11111111;
var fraction = (u64 << 12) >> 12;
var builder = new StringBuilder();
builder.Append(sign != 0 ? " -1" : " 1");
builder.Append(" * 2 ^ (");
// exponent == 0 => subnormals
builder.Append($"{(exponent == 0 ? 1 : exponent),4} - 1023) * 0b{(exponent == 0 ? '0' : '1')}.");
var leadingFractionZero = BitOperations.LeadingZeroCount(fraction) - 12;
builder.Append('0', leadingFractionZero);
if (fraction != 0) builder.Append(Convert.ToString((long)fraction, 2));
WriteHighlightLine($" = {builder}");
}
private void WriteFloat(float f32)
{
var u32 = (uint)BitConverter.SingleToInt32Bits(f32);
WriteHighlightLine($" # IEEE 754 - float - 32-bit");
WriteHighlightLine($" #");
WriteInt32(u32, true);
// Prints the 16 bits indices
WriteHighlightLine($" # 31 24 16 8 0");
WriteHighlightLine($" #");
WriteHighlightLine($" # sign exponent |------ fraction -----|");
// # 1 * 2 ^ (127 - 127) * 0b1.10000000000000000000000
var sign = (u32 >> 31);
var exponent = (u32 >> 23) & 0b_111_11111111;
var fraction = (u32 << 9) >> 9;
var builder = new StringBuilder();
builder.Append(sign != 0 ? " -1" : " 1");
builder.Append(" * 2 ^ (");
// exponent == 0 => subnormals
builder.Append($"{(exponent == 0 ? 1 : exponent),3} - 127) * 0b{(exponent == 0 ? '0' : '1')}.");
var leadingFractionZero = BitOperations.LeadingZeroCount(fraction) - 9;
builder.Append('0', leadingFractionZero);
if (fraction != 0) builder.Append(Convert.ToString((long)fraction, 2));
WriteHighlightLine($" = {builder}f");
}
public void WriteHighlightAligned(string prefix, string text, string nextPrefix = null)
{
var list = SplitStringBySpaceAndKeepSpace(text);
if (list.Count == 0) return;
var builder = new StringBuilder();
bool lineHasItem = false;
var maxColumn = IsOutputSupportHighlighting ? Math.Min(Config.HelpMaxColumn, Console.BufferWidth) : Config.HelpMaxColumn;
if (nextPrefix == null)
{
nextPrefix = prefix.StartsWith("#") ? "#" + new string(' ', prefix.Length - 1) : new string(' ', prefix.Length);
}
bool isFirstItem = false;
int index = 0;
while(index < list.Count)
{
var item = list[index];
if (builder.Length == 0)
{
lineHasItem = false;
builder.Append(prefix);
prefix = nextPrefix;
isFirstItem = true;
}
var nextLineLength = builder.Length + item.Length + 2;
if (item != "\n" && (nextLineLength < maxColumn || !lineHasItem))
{
if (isFirstItem && string.IsNullOrWhiteSpace(item))
{
// Don't append a space at the beginning of a line
}
else
{
builder.Append(item);
lineHasItem = true;
isFirstItem = false;
}
index++;
}
else
{
WriteHighlightLine(builder.ToString());
if (item == "\n") index++;
builder.Length = 0;
}
}
if (builder.Length > 0)
{
WriteHighlightLine(builder.ToString());
}
}
internal void WriteErrorLine(string scriptText)
{
if (scriptText == null) throw new ArgumentNullException(nameof(scriptText));
if (_isInitializing)
{
throw new InvalidOperationException($"Unexpected error while initializing: {scriptText}");
}
if (IsOutputSupportHighlighting)
{
HighlightOutput.Append(ConsoleStyle.Red, true);
HighlightOutput.Append(scriptText);
HighlightOutput.Append(ConsoleStyle.Red, false);
}
else
{
BufferedErrorWriter.Write(scriptText);
}
var previousEcho = EchoEnabled;
try
{
EchoEnabled = true;
WriteHighlightLine(true);
}
finally
{
EchoEnabled = previousEcho;
}
}
internal void WriteHighlightLine(string scriptText, bool highlight = true)
{
WriteHighlight(scriptText, highlight);
WriteHighlightLine();
}
internal void WriteHighlightLine(bool error = false)
{
if (!EchoEnabled) return;
var writer = error ? BufferedErrorWriter : BufferedOutputWriter;
if (HasInteractiveConsole && _isFirstWriteForEval)
{
writer.WriteLine();
_isFirstWriteForEval = false;
}
if (IsOutputSupportHighlighting)
{
HighlightOutput.AppendLine();
HighlightOutput.Render(writer, true);
HighlightOutput.Clear();
}
else
{
writer.WriteLine();
}
writer.Commit();
Repl?.Reset();
}
internal void WriteHighlight(string scriptText, bool highlight = true)
{
if (!EchoEnabled) return;
if (IsOutputSupportHighlighting)
{
_tempOutputHighlight.Clear();
_tempOutputHighlight.ClearStyles();
_tempOutputHighlight.Append(scriptText);
if (highlight)
{
// Highlight line per line
Highlight(_tempOutputHighlight);
}
HighlightOutput.AddRange(_tempOutputHighlight);
}
else
{
BufferedOutputWriter.Write(scriptText);
}
}
public void Highlight(ConsoleText text, int cursorIndex = -1)
{
if (text == null) throw new ArgumentNullException(nameof(text));
var textStr = text.ToString();
var lexer = new Lexer(textStr, options: _lexerOptions);
var tokens = lexer.ToList();
UpdateSyntaxHighlighting(text, textStr, tokens);
if (cursorIndex >= 0)
{
UpdateMatchingBraces(cursorIndex, text, tokens);
}
}
private void UpdateSyntaxHighlighting(ConsoleText text, string textStr, List<Token> tokens)
{
if (textStr == null) return;
bool isPreviousNotDot = true;
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
var styleOpt = GetStyle(i, token, textStr, isPreviousNotDot, tokens);
isPreviousNotDot = token.Type != TokenType.Dot;
if (styleOpt.HasValue)
{
var style = styleOpt.Value;
var start = token.Start.Offset;
var end = token.End.Offset;
if (start >= 0 && end >= 0 && start <= end && end < text.Count)
{
text.EnableStyleAt(start, style);
text.DisableStyleAt(end + 1, style);
}
}
}
}
private void UpdateMatchingBraces(int cursorIndex, ConsoleText text, List<Token> tokens)
{
// Collect all braces open/close
List<(int, int)> matchingBraces = null;
List<int> pendingUnpairedBraces = null;
List<int> unpairedBraces = null;
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
for (var j = 0; j < MatchPairs.Length; j++)
{
var match = MatchPairs[j];
if (match == token.Type)
{
if (pendingUnpairedBraces == null)
{
matchingBraces = new List<(int, int)>();
pendingUnpairedBraces = new List<int>();
unpairedBraces = new List<int>();
}
bool processed = false;
bool isOpening = (j & 1) == 0;
if (pendingUnpairedBraces.Count > 0)
{
var toMatch = isOpening ? MatchPairs[j + 1] : MatchPairs[j - 1];
var leftIndex = pendingUnpairedBraces[pendingUnpairedBraces.Count - 1];
if (tokens[leftIndex].Type == toMatch)
{
pendingUnpairedBraces.RemoveAt(pendingUnpairedBraces.Count - 1);
matchingBraces.Add((leftIndex, i));
processed = true;
}
}
if (!processed)
{
if (isOpening)
{
pendingUnpairedBraces.Add(i);
}
else
{
// Closing that are not matching will never match, put them at the front of the braces that will never match
unpairedBraces.Add(i);
}
}
break;
}
}
}
if (matchingBraces == null) return;
unpairedBraces.AddRange(pendingUnpairedBraces);
var tokenIndex = FindTokenIndexFromColumnIndex(cursorIndex, text.Count, tokens);
var matchingCurrent = matchingBraces.Where(x => x.Item1 == tokenIndex || x.Item2 == tokenIndex).Select(x => ((int, int)?) x).FirstOrDefault();
// Color current matching braces
if (matchingCurrent.HasValue)
{
var leftIndex = tokens[matchingCurrent.Value.Item1].Start.Column;
var rightIndex = tokens[matchingCurrent.Value.Item2].Start.Column;
text.EnableStyleAt(leftIndex, ConsoleStyle.Underline);
text.EnableStyleAt(leftIndex, ConsoleStyle.Bold);
text.DisableStyleAt(leftIndex + 1, ConsoleStyle.Bold);
text.DisableStyleAt(leftIndex + 1, ConsoleStyle.Underline);
text.EnableStyleAt(rightIndex, ConsoleStyle.Underline);
text.EnableStyleAt(rightIndex, ConsoleStyle.Bold);
text.DisableStyleAt(rightIndex + 1, ConsoleStyle.Bold);
text.DisableStyleAt(rightIndex + 1, ConsoleStyle.Underline);
}
// Color any braces that are not matching
foreach (var invalid in unpairedBraces)
{
var invalidIndex = tokens[invalid].Start.Column;
if (invalid == tokenIndex) text.EnableStyleAt(invalidIndex, ConsoleStyle.Underline);
text.EnableStyleAt(invalidIndex, ConsoleStyle.BrightRed);
text.DisableStyleAt(invalidIndex + 1, ConsoleStyle.BrightRed);
if (invalid == tokenIndex) text.DisableStyleAt(invalidIndex + 1, ConsoleStyle.Underline);
}
}
private int FindTokenIndexFromColumnIndex(int index, int textCount, List<Token> tokens)
{
for (var i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
var start = token.Start.Column;
var end = token.End.Column;
if (start >= 0 && end >= 0 && start <= end && end < textCount)
{
if (index >= start && index <= end)
{
return i;
}
}
}
return -1;
}
private ConsoleStyle? GetStyle(int index, Token token, string text, bool isPreviousNotDot, List<Token> tokens)
{
switch (token.Type)
{
case TokenType.Integer:
case TokenType.HexaInteger:
case TokenType.BinaryInteger:
case TokenType.Float:
return ConsoleStyle.Bold;
case TokenType.String:
case TokenType.VerbatimString:
return ConsoleStyle.BrightCyan;
case TokenType.Comment:
case TokenType.CommentMulti:
return ConsoleStyle.BrightGreen;
case TokenType.Identifier:
var key = token.GetText(text);
if (isPreviousNotDot)
{
if ((index + 1 < tokens.Count && tokens[index + 1].Type == TokenType.Colon))
{
return ConsoleStyle.BrightWhite;
}
if (ScriptKeywords.Contains(key))
{
// Handle the case where for ... in, the in should be marked as a keyword
if (key != "in" || !(index > 1 && tokens[index - 2].GetText(text) == "for"))
{
return ConsoleStyle.BrightYellow;
}
}
if (Builtins.ContainsKey(key) || ValueKeywords.Contains(key))
{
return ConsoleStyle.Cyan;
}
else if (Units.ContainsKey(key))
{
return ConsoleStyle.Yellow;
}
}
return ConsoleStyle.White;
default:
return null;
}
}
}
}

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Consolus;
using Scriban;
using Scriban.Functions;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine
{
private Stopwatch _clockReplInput;
public ConsoleRepl Repl { get; private set; }
public int OnErrorToNextLineMaxDelayInMilliseconds { get; set; }
private void InitializeRepl()
{
_clockReplInput = Stopwatch.StartNew();
OnErrorToNextLineMaxDelayInMilliseconds = 300;
OnClearScreen = ClearScreen;
Repl.GetClipboardTextImpl = GetClipboardText;
Repl.SetClipboardTextImpl = SetClipboardText;
Repl.BeforeRender = OnBeforeRendering;
Repl.GetCancellationTokenSource = () => _cancellationTokenSource;
Repl.TryPreProcessKey = TryPreProcessKey;
Repl.OnTextValidatingEnter = OnTextValidatingEnter;
Repl.Prompt.Clear();
Repl.Prompt.Begin(ConsoleStyle.BrightBlack).Append(">>> ").Append(ConsoleStyle.BrightBlack, false);
}
private bool OnTextValidatingEnter(string text, bool hasControl)
{
try
{
return OnTextValidatingEnterInternal(text, hasControl);
}
finally
{
_clockReplInput.Restart();
}
}
private bool OnTextValidatingEnterInternal(string text, bool hasControl)
{
_cancellationTokenSource = new CancellationTokenSource();
CancellationToken = _cancellationTokenSource.Token;
var elapsed = _clockReplInput.ElapsedMilliseconds;
Template script = null;
object result = null;
string error = null;
int column = -1;
bool isCancelled = false;
try
{
script = Parse(text);
if (script.HasErrors)
{
var errorBuilder = new StringBuilder();
foreach (var message in script.Messages)
{
if (errorBuilder.Length > 0) errorBuilder.AppendLine();
if (column <= 0 && message.Type == ParserMessageType.Error)
{
column = message.Span.Start.Column;
}
errorBuilder.Append(message.Message);
}
error = errorBuilder.ToString();
}
else
{
Repl.AfterEditLine.Clear();
HighlightOutput.Clear();
result = EvaluatePage(script.Page);
if (Repl.ExitOnNextEval)
{
return false;
}
}
}
catch (Exception ex)
{
if (ex is ScriptRuntimeException scriptEx)
{
column = scriptEx.Span.Start.Column;
error = scriptEx.OriginalMessage;
isCancelled = ex is ScriptAbortException;
}
else
{
error = ex.Message;
}
}
if (error != null)
{
Repl.AfterEditLine.Clear();
Repl.AfterEditLine.Append('\n');
Repl.AfterEditLine.Begin(ConsoleStyle.Red);
Repl.AfterEditLine.Append(error);
if (column >= 0 && column <= Repl.EditLine.Count)
{
Repl.EnableCursorChanged = false;
Repl.CursorIndex = column;
Repl.EnableCursorChanged = true;
}
bool emitReturnOnError = hasControl || elapsed < OnErrorToNextLineMaxDelayInMilliseconds || isCancelled;
if (emitReturnOnError)
{
Repl.AfterEditLine.Append('\n');
}
return emitReturnOnError;
}
else
{
if (result != null)
{
Write(script.Page.Span, result);
}
var resultStr = Output.ToString();
var output = Output as StringBuilderOutput;
if (output != null)
{
output.Builder.Length = 0;
}
Repl.AfterEditLine.Clear();
bool hasOutput = resultStr != string.Empty || HighlightOutput.Count > 0;
if (!Repl.IsClean || hasOutput)
{
Repl.AfterEditLine.Append('\n');
bool hasNextOutput = HighlightOutput.Count > 0;
if (hasNextOutput)
{
Repl.AfterEditLine.AddRange(HighlightOutput);
HighlightOutput.Clear();
}
if (resultStr != string.Empty)
{
if (hasNextOutput)
{
Repl.AfterEditLine.AppendLine();
}
Repl.AfterEditLine.Begin(ConsoleStyle.Bold);
Repl.AfterEditLine.Append(resultStr);
}
if (hasOutput)
{
Repl.AfterEditLine.AppendLine();
}
}
}
return true;
}
private void OnEnterNextText(string textToEnter)
{
Repl?.EnqueuePendingTextToEnter(textToEnter);
}
private bool TryPreProcessKey(ConsoleKeyInfo arg, ref int cursorIndex)
{
return OnKey(arg, Repl.EditLine, ref cursorIndex);
}
private void OnBeforeRendering()
{
UpdateSyntaxHighlighting();
}
private void UpdateSyntaxHighlighting()
{
if (Repl != null)
{
Repl.EditLine.ClearStyles();
Highlight(Repl.EditLine, Repl.CursorIndex);
}
}
public void ClearScreen()
{
Repl?.Clear();
HighlightOutput.Clear();
}
public void ReplExit()
{
if (Repl == null) return;
Repl.ExitOnNextEval = true;
}
internal bool OnKey(ConsoleKeyInfo arg, ConsoleText line, ref int cursorIndex)
{
bool resetMap = true;
bool resetCompletion = true;
try
{
bool hasShift = (arg.Modifiers & ConsoleModifiers.Shift) != 0;
// For now, we discard SHIFT entirely, as it is handled separately for range selection
// TODO: we should handle them not differently from ctrl/alt, but it's complicated
arg = new ConsoleKeyInfo(arg.KeyChar, arg.Key, false, (arg.Modifiers & ConsoleModifiers.Alt) != 0, (arg.Modifiers & ConsoleModifiers.Control) != 0);
KalkConsoleKey kalkKey = arg;
if (cursorIndex >= 0 && cursorIndex <= line.Count)
{
if (_currentShortcutKeyMap.TryGetValue(kalkKey, out var value))
{
if (value is KalkShortcutKeyMap map)
{
_currentShortcutKeyMap = map;
resetMap = false; // we don't reset if
}
else
{
var expression = (ScriptExpression) value;
var result = EvaluateExpression(expression);
if (result is KalkActionObject command)
{
// Particular case the completion action, we handle it here
if (command.Action == "completion")
{
// In case of shift we go backward
if (OnCompletionRequested(hasShift, line, ref cursorIndex))
{
resetCompletion = false;
return true;
}
}
else if (OnAction != null)
{
command.Call(OnAction);
}
}
else if (result != null)
{
var resultStr = ObjectToString(result);
line.Insert(cursorIndex, resultStr);
cursorIndex += resultStr.Length;
}
}
return true;
}
}
}
finally
{
if (resetMap)
{
// Restore the root key map in case of an error.
_currentShortcutKeyMap = Shortcuts.ShortcutKeyMap;
}
if (resetCompletion)
{
ResetCompletion();
}
}
return false;
}
private bool OnCompletionRequested(bool backward, ConsoleText line, ref int cursorIndex)
{
// Nothing to complete
if (cursorIndex == 0) return false;
// We expect to have at least:
// - one letter identifier before the before the cursor
// - no letter identifier on the cursor (e.g middle of an existing identifier)
if (cursorIndex < line.Count && IsIdentifierLetter(line[cursorIndex].Value) || !IsIdentifierLetter(line[cursorIndex - 1].Value))
{
return false;
}
// _currentIndexInCompletionMatchingList
if (_currentIndexInCompletionMatchingList < 0)
{
if (!CollectCompletionList(line, cursorIndex))
{
return false;
}
}
// Go to next word
_currentIndexInCompletionMatchingList = (_currentIndexInCompletionMatchingList + (backward ? -1 : 1));
// Wrap the result
if (_currentIndexInCompletionMatchingList >= _completionMatchingList.Count) _currentIndexInCompletionMatchingList = 0;
if (_currentIndexInCompletionMatchingList < 0) _currentIndexInCompletionMatchingList = _completionMatchingList.Count - 1;
if (_currentIndexInCompletionMatchingList < 0 || _currentIndexInCompletionMatchingList >= _completionMatchingList.Count) return false;
var index = _startIndexForCompletion;
var newText = _completionMatchingList[_currentIndexInCompletionMatchingList];
line.RemoveRangeAt(index, cursorIndex - index);
line.Insert(index, newText);
cursorIndex = index + newText.Length;
return true;
}
private void ResetCompletion()
{
_currentIndexInCompletionMatchingList = -1;
_completionMatchingList.Clear();
}
private bool CollectCompletionList(ConsoleText line, int cursorIndex)
{
var text = line.ToString();
var lexer = new Lexer(text, options: _lexerOptions);
var tokens = lexer.ToList();
// Find that we are in a correct place
var index = FindTokenIndexFromColumnIndex(cursorIndex, text.Length, tokens);
if (index >= 0)
{
// If we are in the middle of a comment/integer/float/string
// we don't expect to make any completion
var token = tokens[index];
switch (token.Type)
{
case TokenType.Comment:
case TokenType.CommentMulti:
case TokenType.Identifier:
case TokenType.IdentifierSpecial:
case TokenType.Integer:
case TokenType.HexaInteger:
case TokenType.BinaryInteger:
case TokenType.Float:
case TokenType.String:
case TokenType.ImplicitString:
case TokenType.VerbatimString:
return false;
}
}
// Look for the start of the work to complete
_startIndexForCompletion = cursorIndex - 1;
while (_startIndexForCompletion >= 0)
{
var c = text[_startIndexForCompletion];
if (!IsIdentifierLetter(c))
{
break;
}
_startIndexForCompletion--;
}
_startIndexForCompletion++;
if (!IsFirstIdentifierLetter(text[_startIndexForCompletion]))
{
return false;
}
var startTextToFind = text.Substring(_startIndexForCompletion, cursorIndex - _startIndexForCompletion);
Collect(startTextToFind, ScriptKeywords, _completionMatchingList);
Collect(startTextToFind, ValueKeywords, _completionMatchingList);
Collect(startTextToFind, Variables.Keys, _completionMatchingList);
Collect(startTextToFind, Builtins.Keys, _completionMatchingList);
// If we are not able to match anything from builtin and user variables/functions
// continue on units
if (_completionMatchingList.Count == 0)
{
Collect(startTextToFind, Units.Keys, _completionMatchingList);
}
return true;
}
private static void Collect(string startText, IEnumerable<string> keys, List<string> matchingList)
{
foreach (var key in keys.OrderBy(x => x))
{
if (key.StartsWith(startText))
{
matchingList.Add(key);
}
}
}
private static bool IsIdentifierCharacter(char c)
{
var newCategory = GetCharCategory(c);
switch (newCategory)
{
case UnicodeCategory.UppercaseLetter:
case UnicodeCategory.LowercaseLetter:
case UnicodeCategory.TitlecaseLetter:
case UnicodeCategory.ModifierLetter:
case UnicodeCategory.OtherLetter:
case UnicodeCategory.NonSpacingMark:
case UnicodeCategory.DecimalDigitNumber:
case UnicodeCategory.ModifierSymbol:
case UnicodeCategory.ConnectorPunctuation:
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsFirstIdentifierLetter(char c)
{
return c == '_' || char.IsLetter(c);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierLetter(char c)
{
return IsFirstIdentifierLetter(c) || char.IsDigit(c);
}
private static UnicodeCategory GetCharCategory(char c)
{
if (c == '_' || (c >= '0' && c <= '9')) return UnicodeCategory.LowercaseLetter;
c = char.ToLowerInvariant(c);
return char.GetUnicodeCategory(c);
}
}
}

View File

@@ -0,0 +1,198 @@
using System;
using System.IO;
using System.Text;
using Consolus;
using Scriban.Syntax;
namespace Kalk.Core
{
public partial class KalkEngine
{
public bool Run(params string[] args)
{
if (!Console.IsInputRedirected && !Console.IsOutputRedirected && ConsoleHelper.HasInteractiveConsole)
{
Repl = new ConsoleRepl();
HasInteractiveConsole = true;
InitializeRepl();
try
{
if (ConsoleRepl.IsSelf())
{
Console.Title = $"kalk {Version}";
}
}
catch
{
// ignore
}
}
// Directory.CreateDirectory(KalkUserFolder);
if (DisplayVersion)
{
ShowVersion();
WriteHighlightLine("# Type `help` for more information and at https://github.com/xoofx/kalk");
WriteHighlightLine("# Type `exit` or CTRL+Z to exit from kalk");
}
// Load user config file after showing the version
LoadUserConfigFile();
if (Repl != null)
{
return RunInteractive();
}
else
{
return RunNonInteractive();
}
}
private bool RunInteractive()
{
try
{
OnAction = InteractiveOnAction;
_clockReplInput.Restart();
Repl.Run();
}
catch (Exception ex)
{
Console.Error.WriteLine($"Unexpected exception {ex}");
return false;
}
return true;
}
private void InteractiveOnAction(KalkAction obj)
{
switch (obj)
{
case KalkAction.Exit:
Repl.Action(ConsoleAction.Exit);
break;
case KalkAction.CursorLeft:
Repl.Action(ConsoleAction.CursorLeft);
break;
case KalkAction.CursorRight:
Repl.Action(ConsoleAction.CursorRight);
break;
case KalkAction.CursorLeftWord:
Repl.Action(ConsoleAction.CursorLeftWord);
break;
case KalkAction.CursorRightWord:
Repl.Action(ConsoleAction.CursorRightWord);
break;
case KalkAction.CursorStartOfLine:
Repl.Action(ConsoleAction.CursorStartOfLine);
break;
case KalkAction.CursorEndOfLine:
Repl.Action(ConsoleAction.CursorEndOfLine);
break;
case KalkAction.HistoryPrevious:
Repl.Action(ConsoleAction.HistoryPrevious);
break;
case KalkAction.HistoryNext:
Repl.Action(ConsoleAction.HistoryNext);
break;
case KalkAction.DeleteCharacterLeft:
Repl.Action(ConsoleAction.DeleteCharacterLeft);
break;
case KalkAction.DeleteCharacterLeftAndCopy:
break;
case KalkAction.DeleteCharacterRight:
Repl.Action(ConsoleAction.DeleteCharacterRight);
break;
case KalkAction.DeleteCharacterRightAndCopy:
break;
case KalkAction.DeleteWordLeft:
Repl.Action(ConsoleAction.DeleteWordLeft);
break;
case KalkAction.DeleteWordRight:
Repl.Action(ConsoleAction.DeleteWordRight);
break;
case KalkAction.Completion:
Repl.Action(ConsoleAction.Completion);
break;
case KalkAction.DeleteTextRightAndCopy:
break;
case KalkAction.DeleteWordRightAndCopy:
break;
case KalkAction.DeleteWordLeftAndCopy:
break;
case KalkAction.CopySelection:
Repl.Action(ConsoleAction.CopySelection);
break;
case KalkAction.CutSelection:
Repl.Action(ConsoleAction.CutSelection);
break;
case KalkAction.PasteClipboard:
Repl.Action(ConsoleAction.PasteClipboard);
break;
case KalkAction.ValidateLine:
Repl.Action(ConsoleAction.ValidateLine);
break;
case KalkAction.ForceValidateLine:
Repl.Action(ConsoleAction.ForceValidateLine);
break;
default:
throw new ArgumentOutOfRangeException(nameof(obj), obj, null);
}
}
private bool RunNonInteractive()
{
bool success = true;
string line;
while ((line = InputReader.ReadLine()) != null)
{
if (EchoEnabled && EchoInput) OutputWriter.Write($">>> {line}");
try
{
var script = Parse(line);
if (script.HasErrors)
{
//throw new ScriptParserRuntimeException();
var errorBuilder = new StringBuilder();
foreach (var message in script.Messages)
{
if (errorBuilder.Length > 0) errorBuilder.AppendLine();
errorBuilder.Append(message.Message);
}
var error = errorBuilder.ToString();
throw new InvalidOperationException(error);
}
else
{
var result = EvaluatePage(script.Page);
}
}
catch (Exception ex)
{
if (ex is ScriptRuntimeException runtimeEx)
{
WriteErrorLine(runtimeEx.OriginalMessage);
}
else
{
WriteErrorLine(ex.Message);
}
//Console.WriteLine(ex.InnerException);
success = false;
break;
}
if (HasExit) break;
}
return success;
}
}
}

View File

@@ -0,0 +1,502 @@
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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
using System;
namespace Kalk.Core
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)]
public class KalkExportAttribute : System.Attribute
{
public KalkExportAttribute(string alias, string category)
{
Alias = alias;
Category = category;
}
public string Alias { get; }
public string Category { get; }
public bool Functor { get; set; }
}
[AttributeUsage(AttributeTargets.Class)]
public class KalkExportModuleAttribute : System.Attribute
{
public KalkExportModuleAttribute(string alias)
{
Alias = alias;
}
public string Alias { get; }
}
}

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public abstract class KalkModule : IScriptCustomFunction, IScriptObject
{
protected KalkModule() : this(null)
{
}
protected KalkModule(string name)
{
Name = name;
if (Name == null)
{
Name = this.GetType().Name;
if (Name.EndsWith("Module"))
{
Name = Name.Substring(0, Name.Length - "Module".Length);
}
}
Descriptors = new Dictionary<string, KalkDescriptor>();
Descriptor = new KalkDescriptor() { Names = { Name } };
}
public string Name { get; private set; }
public KalkEngine Engine { get; private set; }
public bool IsImported { get; private set; }
public bool IsBuiltin { get; protected set; }
public bool IsInitialized { get; private set; }
public KalkDescriptor Descriptor { get; private set; }
internal void Initialize(KalkEngine engine)
{
if (IsInitialized) return;
Engine = engine;
Initialize();
IsInitialized = true;
}
internal void InternalImport()
{
if (IsImported) return;
Import();
IsImported = true;
}
protected virtual void Initialize()
{
}
protected virtual void Import()
{
foreach (var descriptor in Descriptors)
{
Engine.Descriptors[descriptor.Key] = descriptor.Value;
}
}
public Dictionary<string, KalkDescriptor> Descriptors { get; }
int IScriptFunctionInfo.RequiredParameterCount => 0;
int IScriptFunctionInfo.ParameterCount => 0;
ScriptVarParamKind IScriptFunctionInfo.VarParamKind => ScriptVarParamKind.None;
Type IScriptFunctionInfo.ReturnType => typeof(object);
ScriptParameterInfo IScriptFunctionInfo.GetParameterInfo(int index) => throw new NotSupportedException("A module doesn't have any parameters.");
object IScriptCustomFunction.Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
var engine = (KalkEngine) context;
engine.WriteHighlightLine($"# Module {Name}");
return null;
}
ValueTask<object> IScriptCustomFunction.InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(((IScriptCustomFunction)this).Invoke(context, callerContext, arguments, blockStatement));
}
int IScriptObject.Count => 0;
IEnumerable<string> IScriptObject.GetMembers() => Enumerable.Empty<string>();
bool IScriptObject.Contains(string member) => false;
bool IScriptObject.IsReadOnly
{
get => true;
set
{
}
}
bool IScriptObject.TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
{
value = null;
return false;
}
bool IScriptObject.CanWrite(string member) => false;
bool IScriptObject.TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) => false;
bool IScriptObject.Remove(string member) => false;
void IScriptObject.SetReadOnly(string member, bool readOnly)
{
}
IScriptObject IScriptObject.Clone(bool deep)
{
throw new NotSupportedException("A module does not support the clone operation.");
}
}
}

View File

@@ -0,0 +1,126 @@
using System;
using Scriban.Runtime;
namespace Kalk.Core
{
public abstract class KalkModuleWithFunctions : KalkModule
{
protected KalkModuleWithFunctions() : this(null)
{
}
protected KalkModuleWithFunctions(string name) : base(name)
{
Content = new ScriptObject();
}
public ScriptObject Content { get; }
protected override void Import()
{
base.Import();
// Feed the engine with our new builtins
Engine.Builtins.Import(Content);
DisplayImport();
}
protected virtual void DisplayImport()
{
if (!IsBuiltin && Content.Count > 0)
{
Engine.WriteHighlightLine($"# {Content.Count} functions successfully imported from module `{Name}`.");
}
}
protected virtual void RegisterFunctionsAuto()
{
}
protected void RegisterConstant(string name, object value, string category = null)
{
RegisterVariable(name, value, category);
}
protected void RegisterAction(string name, Action action, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1>(string name, Action<T1> action, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2>(string name, Action<T1, T2> action, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2, T3>(string name, Action<T1, T2, T3> action, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterAction<T1, T2, T3, T4>(string name, Action<T1, T2, T3, T4> action, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.Create(action), category);
}
protected void RegisterFunction<T1>(string name, Func<T1> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterFunction<T1, T2>(string name, Func<T1, T2> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterFunction<T1, T2, T3>(string name, Func<T1, T2, T3> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterFunction<T1, T2, T3, T4>(string name, Func<T1, T2, T3, T4> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterFunction<T1, T2, T3, T4, T5>(string name, Func<T1, T2, T3, T4, T5> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterFunction<T1, T2, T3, T4, T5, T6>(string name, Func<T1, T2, T3, T4, T5, T6> func, string category = null)
{
RegisterCustomFunction(name, DelegateCustomFunction.CreateFunc(func), category);
}
protected void RegisterCustomFunction(string name, IScriptCustomFunction func, string category = null)
{
RegisterVariable(name, func, category);
}
protected void RegisterVariable(string name, object value, string category = null)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (value == null) throw new ArgumentNullException(nameof(value));
var names = name.Split(',');
KalkDescriptor descriptor = null;
foreach (var subName in names)
{
Content.SetValue(subName, value, true);
if (descriptor == null || !Descriptors.TryGetValue(names[0], out descriptor))
{
descriptor = new KalkDescriptor();
}
Descriptors.Add(subName, descriptor);
descriptor.Names.Add(subName);
}
}
}
}

View File

@@ -0,0 +1,55 @@
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
namespace Kalk.Core
{
public class KalkObjectWithAlias : ScriptObject
{
public KalkObjectWithAlias()
{
}
public KalkObjectWithAlias(KalkEngine engine)
{
Engine = engine;
}
public KalkEngine Engine { get; set; }
public override bool Contains(string member)
{
member = Alias(member);
return base.Contains(member);
}
public override bool CanWrite(string member)
{
member = Alias(member);
return base.CanWrite(member);
}
public override bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
{
member = Alias(member);
return base.TryGetValue(context, span, member, out value);
}
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
{
member = Alias(member);
return base.TrySetValue(context, span, member, value, readOnly);
}
private string Alias(string name)
{
// Replace with an alias
if (Engine.Aliases.TryGetAlias(name, out var alias))
{
name = alias;
}
return name;
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace Kalk.Core
{
public class KalkSettings
{
public static void Initialize()
{
var userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Kalk.Core
{
public class KalkShortcutKeyMap : Dictionary<KalkConsoleKey, object>
{
public bool TryGetShortcut(KalkConsoleKey key, out object next)
{
return TryGetValue(key, out next);
}
}
}

View File

@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkShortcuts : ScriptObject, IScriptCustomFunction
{
public KalkShortcuts()
{
ShortcutKeyMap = new KalkShortcutKeyMap();
}
public KalkShortcutKeyMap ShortcutKeyMap { get; }
public int RequiredParameterCount => 0;
public int ParameterCount => 0;
public ScriptVarParamKind VarParamKind => ScriptVarParamKind.None;
public Type ReturnType => typeof(object);
public ScriptParameterInfo GetParameterInfo(int index)
{
throw new NotSupportedException("Shortcuts don't have any parameters.");
}
public new void Clear()
{
base.Clear();
ShortcutKeyMap.Clear();
}
public void RemoveShortcut(string name)
{
if (!TryGetValue(name, out var shortcut)) return;
Remove(name);
foreach (KalkShortcutKeySequence shortcutKey in shortcut.Keys)
{
var map = ShortcutKeyMap;
KalkConsoleKey consoleKey = default;
for (int i = 0; i < shortcutKey.Keys.Count; i++)
{
if (i > 0)
{
if (!map.TryGetShortcut(consoleKey, out var newMap) || !(newMap is KalkShortcutKeyMap))
{
continue;
}
map = (KalkShortcutKeyMap)newMap;
}
consoleKey = shortcutKey.Keys[i];
}
map.Remove(consoleKey);
}
// Cleanup all maps to remove empty ones
CleanupMap(ShortcutKeyMap);
}
private void CleanupMap(KalkShortcutKeyMap map)
{
var keyParis = map.ToList();
foreach (var keyPair in keyParis)
{
if (keyPair.Value is KalkShortcutKeyMap nestedMap)
{
CleanupMap(nestedMap);
if (nestedMap.Count == 0)
{
map.Remove(keyPair.Key);
}
}
}
}
public void SetSymbolShortcut(KalkShortcut shortcut)
{
if (shortcut == null) throw new ArgumentNullException(nameof(shortcut));
RemoveShortcut(shortcut.Name);
Add(shortcut.Name, shortcut);
foreach (KalkShortcutKeySequence shortcutKey in shortcut.Keys)
{
var map = ShortcutKeyMap;
KalkConsoleKey consoleKey = default;
for (int i = 0; i < shortcutKey.Keys.Count; i++)
{
if (i > 0)
{
if (!map.TryGetShortcut(consoleKey, out var newMap) || !(newMap is KalkShortcutKeyMap))
{
newMap = new KalkShortcutKeyMap();
map[consoleKey] = newMap;
}
map = (KalkShortcutKeyMap)newMap;
}
consoleKey = shortcutKey.Keys[i];
}
map[consoleKey] = shortcutKey.Expression;
}
}
public bool TryGetValue(string key, out KalkShortcut value)
{
value = null;
if (TryGetValue(null, new SourceSpan(), key, out var valueObj))
{
value = (KalkShortcut) valueObj;
return true;
}
return false;
}
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
{
// In the case of using KalkSymbols outside of the scripting engine
if (context == null) return base.TrySetValue(null, span, member, value, readOnly);
// Otherwise, we are not allowing to modify this object.
throw new ScriptRuntimeException(span, "Shortcuts object can't be modified directly. You need to use the command `shortcut` instead.");
}
public object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
Display((KalkEngine)context, "Builtin Shortcuts", filter => !filter.IsUser);
Display((KalkEngine) context, "User-defined Shortcuts", filter => filter.IsUser);
return null;
}
public void Display(KalkEngine engine, string title, Func<KalkShortcut, bool> filter = null, bool addBlankLine = false)
{
if (engine == null) throw new ArgumentNullException(nameof(engine));
if (title == null) throw new ArgumentNullException(nameof(title));
var alreadyPrinted = new HashSet<KalkShortcut>();
bool isFirst = true;
foreach (var unitKey in this.Keys.OrderBy(x => x))
{
var shortcut = this[unitKey] as KalkShortcut;
if (shortcut == null || !alreadyPrinted.Add(shortcut) || (filter != null && !filter(shortcut))) continue;
if (isFirst)
{
engine.WriteHighlightLine($"# {title}");
}
else if (addBlankLine)
{
engine.WriteHighlightLine("");
}
isFirst = false;
engine.WriteHighlightLine(shortcut.ToString());
}
}
public ValueTask<object> InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(Invoke(context, callerContext, arguments, blockStatement));
}
}
}

104
Kalk/Kalk.Core/KalkUnits.cs Normal file
View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkUnits : KalkObjectWithAlias, IScriptCustomFunction
{
public int RequiredParameterCount => 0;
public int ParameterCount => 0;
public ScriptVarParamKind VarParamKind => ScriptVarParamKind.None;
public Type ReturnType => typeof(object);
public KalkUnits(KalkEngine engine) : base(engine)
{
}
public ScriptParameterInfo GetParameterInfo(int index)
{
throw new NotSupportedException("Units don't have any parameters.");
}
public bool TryGetValue(string key, out KalkUnit value)
{
value = null;
if (TryGetValue(null, new SourceSpan(), key, out var valueObj))
{
value = (KalkUnit) valueObj;
return true;
}
return false;
}
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly)
{
// In the case of using KalkUnits outside of the scripting engine
if (context == null) return base.TrySetValue(null, span, member, value, readOnly);
// Otherwise, we are not allowing to modify this object.
throw new ScriptRuntimeException(span, "Units object can't be modified directly. You need to use the command `unit` instead.");
}
public object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
var engine = (KalkEngine) context;
if (this.All(x => x.Value.GetType() != typeof(KalkUnit)))
{
engine.WriteHighlightLine($"# No Units defined (e.g try `import StandardUnitsModule`)");
}
else
{
Display((KalkEngine) context, "Builtin Units", symbol => symbol.GetType() == typeof(KalkUnit) && !symbol.IsUser);
Display((KalkEngine) context, "User Defined Units", symbol => symbol.GetType() == typeof(KalkUnit) && symbol.IsUser);
}
return null;
}
public void Display(KalkEngine engine, string title, Func<KalkUnit, bool> filter, bool addBlankLine = true)
{
if (engine == null) throw new ArgumentNullException(nameof(engine));
if (title == null) throw new ArgumentNullException(nameof(title));
var alreadyPrinted = new HashSet<KalkUnit>();
bool isFirst = true;
foreach (var unitKey in this.Keys.OrderBy(x => x))
{
var unit = this[unitKey] as KalkUnit;
if (unit == null || !alreadyPrinted.Add(unit) || unit.Parent != null || (filter != null && !filter(unit))) continue;
if (isFirst)
{
engine.WriteHighlightLine($"# {title}");
}
else if (addBlankLine)
{
engine.WriteHighlightLine("");
}
isFirst = false;
unit.Invoke(engine, engine.CurrentNode, null, null);
}
}
public ValueTask<object> InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(Invoke(context, callerContext, arguments, blockStatement));
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Kalk.Core
{
public interface IKalkDisplayable
{
void Display(KalkEngine engine, KalkDisplayMode mode);
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Text;
using Scriban.Runtime;
namespace Kalk.Core
{
public sealed class KalkAlias : ScriptObject
{
public KalkAlias(string name, IEnumerable<string> aliases, bool isUser)
{
if (aliases == null) throw new ArgumentNullException(nameof(aliases));
Name = name ?? throw new ArgumentNullException(nameof(name));
Aliases = new ScriptArray(aliases) {IsReadOnly = true};
SetValue("name", Name, true);
SetValue("aliases", Aliases, true);
IsReadOnly = true;
IsUser = isUser;
}
public string Name { get; }
public ScriptArray Aliases { get; }
public bool IsUser { get; }
public override string ToString(string format, IFormatProvider formatProvider)
{
var builder = new StringBuilder();
builder.Append($"alias({Name}, ");
for (var i = 0; i < Aliases.Count; i++)
{
var alias = Aliases[i];
if (i > 0) builder.Append(", ");
builder.Append(alias);
}
builder.Append(")");
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkBinaryExpression : KalkExpressionWithMembers
{
private static readonly (string, Func<KalkExpressionWithMembers, object> getter)[] MemberDefs = {
("kind", unit => unit.TypeName),
("value", unit => ((KalkBinaryExpression)unit).Value),
("operator", unit => ((KalkBinaryExpression)unit).OperatorText),
("unit", unit => ((KalkBinaryExpression)unit).Unit),
};
public KalkBinaryExpression()
{
}
public KalkBinaryExpression(object value, ScriptBinaryOperator @operator, object unit)
{
Value = value;
Operator = @operator;
Unit = unit;
}
public override string TypeName => "binary expression";
public object Value { get; set; }
/// <summary>
/// Gets or sets the operator. Supported: /, *, ^
/// </summary>
public ScriptBinaryOperator Operator { get; set; }
public string OperatorText => Operator.ToText();
public object Unit { get; set; }
public override object GetValue() => Value;
public override KalkUnit GetUnit() => Unit as KalkUnit;
public override string ToString(string format, IFormatProvider formatProvider)
{
switch (Operator)
{
case ScriptBinaryOperator.Power:
return (Value is ScriptBinaryExpression) ? $"({string.Format(formatProvider, "{0}",Value)}){OperatorText}{string.Format(formatProvider, "{0}", Unit)}" : $"{string.Format(formatProvider, "{0}", Value)}{OperatorText}{string.Format(formatProvider, "{0}", Unit)}";
default:
return $"{string.Format(formatProvider, "{0}", Value)} {OperatorText} {string.Format(formatProvider, "{0}", Unit)}";
}
}
protected override (string, Func<KalkExpressionWithMembers, object> getter)[] Members => MemberDefs;
protected override bool EqualsImpl(TemplateContext context, KalkExpression other)
{
var otherBin = (KalkBinaryExpression) other;
if (Operator != otherBin.Operator) return false;
if (Value is KalkExpression && otherBin.Value.GetType() != Value.GetType()) return false;
if (Unit is KalkExpression && otherBin.Unit.GetType() != Unit.GetType()) return false;
if (Value is KalkExpression leftExpr)
{
if (!Equals(context, leftExpr, (KalkExpression) otherBin.Value))
{
return false;
}
}
else
{
var leftValue = context.ToObject<double>(context.CurrentSpan, Value);
var otherLeftValue = context.ToObject<double>(context.CurrentSpan, otherBin.Value);
if (!KalkValue.AlmostEqual(leftValue, otherLeftValue))
{
return false;
}
}
if (Unit is KalkExpression rightExpr)
{
if (!Equals(context, rightExpr, (KalkExpression)otherBin.Unit))
{
return false;
}
}
else
{
var rightValue = context.ToObject<double>(context.CurrentSpan, Unit);
var otherRightValue = context.ToObject<double>(context.CurrentSpan, otherBin.Unit);
if (!KalkValue.AlmostEqual(rightValue, otherRightValue))
{
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,214 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Scriban;
using Scriban.Parsing;
using Scriban.Syntax;
namespace Kalk.Core
{
public struct KalkComplex : IScriptCustomType, IKalkSpannable
{
private Complex _value;
private const double MaxRoundToZero = 1e-14;
public KalkComplex(double real, double im)
{
_value = new Complex(real, im);
}
public KalkComplex(Complex value)
{
_value = value;
}
public string TypeName => "complex";
public double Re => _value.Real;
public double Im => _value.Imaginary;
public double Phase => _value.Phase;
public double Magnitude => _value.Magnitude;
public bool HasIm => _value.Imaginary > MaxRoundToZero;
internal Complex Value => _value;
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptBinaryOperator op, SourceSpan leftSpan, object leftValue, SourceSpan rightSpan, object rightValue, out object result)
{
result = null;
if (leftValue is KalkComplex leftComplex && rightValue is KalkComplex rightComplex)
{
switch (op)
{
case ScriptBinaryOperator.Add:
result = (KalkComplex) (leftComplex._value + rightComplex._value);
return true;
case ScriptBinaryOperator.Subtract:
result = (KalkComplex)(leftComplex._value - rightComplex._value);
return true;
case ScriptBinaryOperator.Divide:
result = (KalkComplex)(leftComplex._value / rightComplex._value);
return true;
case ScriptBinaryOperator.DivideRound:
result = (KalkComplex)(leftComplex._value / rightComplex._value);
return true;
case ScriptBinaryOperator.Multiply:
result = (KalkComplex)(leftComplex._value * rightComplex._value);
return true;
case ScriptBinaryOperator.Power:
result = (KalkComplex)(Complex.Pow(leftComplex._value, rightComplex._value));
return true;
}
}
else if (rightValue is KalkComplex rightComplex2)
{
var leftAsDouble = (double)context.ToObject(span, leftValue, typeof(double));
switch (op)
{
case ScriptBinaryOperator.Add:
result = (KalkComplex)(leftAsDouble + rightComplex2._value);
return true;
case ScriptBinaryOperator.Subtract:
result = (KalkComplex)(leftAsDouble - rightComplex2._value);
return true;
case ScriptBinaryOperator.Divide:
result = (KalkComplex)(leftAsDouble / rightComplex2._value);
return true;
case ScriptBinaryOperator.DivideRound:
result = (KalkComplex)(leftAsDouble / rightComplex2._value);
return true;
case ScriptBinaryOperator.Multiply:
result = (KalkComplex)(leftAsDouble * rightComplex2._value);
return true;
}
}
else if (leftValue is KalkComplex leftComplex2)
{
var rightAsDouble = (double)context.ToObject(span, rightValue, typeof(double));
switch (op)
{
case ScriptBinaryOperator.Add:
result = (KalkComplex)(leftComplex2._value + rightAsDouble);
return true;
case ScriptBinaryOperator.Subtract:
result = (KalkComplex)(leftComplex2._value - rightAsDouble);
return true;
case ScriptBinaryOperator.Divide:
result = (KalkComplex)(leftComplex2._value / rightAsDouble);
return true;
case ScriptBinaryOperator.DivideRound:
result = (KalkComplex)(leftComplex2._value / rightAsDouble);
return true;
case ScriptBinaryOperator.Multiply:
result = (KalkComplex)(leftComplex2._value * rightAsDouble);
return true;
case ScriptBinaryOperator.Power:
result = (KalkComplex)(Complex.Pow(leftComplex2._value, rightAsDouble));
return true;
}
}
return false;
}
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptUnaryOperator op, object rightValue, out object result)
{
result = null;
switch (op)
{
case ScriptUnaryOperator.Negate:
result = (KalkComplex) (-((KalkComplex) rightValue)._value);
return true;
case ScriptUnaryOperator.Plus:
result = rightValue;
return true;
}
return false;
}
public static implicit operator KalkComplex(Complex complex)
{
return new KalkComplex(complex);
}
public override string ToString()
{
var builder = new StringBuilder();
// We round the number to make the display nicer
var re = Math.Round(Re, 10, MidpointRounding.AwayFromZero);
var im = Math.Round(Im, 10, MidpointRounding.AwayFromZero);
if (Math.Abs(re) > 1e-14) builder.AppendFormat(CultureInfo.CurrentCulture, "{0}", re);
if (HasIm)
{
if (builder.Length > 0) builder.Append(" + ");
if (Math.Abs(Math.Abs(im) - 1.0) < MaxRoundToZero)
{
builder.Append("i");
}
else
{
builder.AppendFormat(CultureInfo.CurrentCulture, "{0}i", im);
}
}
if (builder.Length == 0) builder.Append("0");
return builder.ToString();
}
public Span<byte> AsSpan()
{
return MemoryMarshal.CreateSpan(ref Unsafe.As<KalkComplex, byte>(ref Unsafe.AsRef(this)), Unsafe.SizeOf<KalkComplex>());
}
public bool TryConvertTo(TemplateContext context, SourceSpan span, Type type, out object value)
{
if (type == typeof(object))
{
value = this;
return true;
}
if (type == typeof(int))
{
if (HasIm) throw new ScriptRuntimeException(span, $"Cannot convert {this} to an integer as it has an imaginary part.");
value = (int) Re;
return true;
}
if (type == typeof(float))
{
if (HasIm) throw new ScriptRuntimeException(span, $"Cannot convert {this} to a float as it has an imaginary part.");
value = (float)Re;
return true;
}
if (type == typeof(double))
{
if (HasIm) throw new ScriptRuntimeException(span, $"Cannot convert {this} to a double as it has an imaginary part.");
value = (double)Re;
return true;
}
if (type == typeof(decimal))
{
if (HasIm) throw new ScriptRuntimeException(span, $"Cannot convert {this} to a decimal as it has an imaginary part.");
value = (decimal)Re;
return true;
}
if (type == typeof(BigInteger))
{
if (HasIm) throw new ScriptRuntimeException(span, $"Cannot convert {this} to a big integer as it has an imaginary part.");
value = (BigInteger)Re;
return true;
}
throw new ScriptRuntimeException(span, $"Cannot convert {this} to an {type} as it has an imaginary part.");
}
}
}

View File

@@ -0,0 +1,217 @@
using System;
using Scriban;
using Scriban.Helpers;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkCompositeValue : IScriptConvertibleFrom, IScriptTransformable
{
public IScriptTransformable Transformable;
public object Value;
public KalkCompositeValue()
{
}
public KalkCompositeValue(object value)
{
Transformable = value as IScriptTransformable;
Value = Transformable == null ? value : null;
}
public KalkCompositeValue(IScriptTransformable transformable)
{
Transformable = transformable;
}
public object TransformArg<TFrom, TTo>(TemplateContext context, Func<TFrom, TTo> apply)
{
try
{
return Transform(context, apply);
}
catch (ScriptRuntimeException ex)
{
throw new ScriptArgumentException(0, ex.OriginalMessage);
}
catch (Exception ex)
{
throw new ScriptArgumentException(0, ex.Message);
}
}
public object Transform<TFrom, TTo>(TemplateContext context, Func<TFrom, TTo> apply)
{
if (Transformable != null)
{
return Transformable.Transform(context, context.CurrentSpan, value =>
{
var nestedValue = (KalkCompositeValue)context.ToObject(context.CurrentSpan, value, this.GetType());
// Recursively apply the transformation with the same value
return nestedValue.Transform(context, apply);
}, typeof(TTo));
}
return apply(context.ToObject<TFrom>(context.CurrentSpan, Value));
}
public virtual Type ElementType => typeof(object);
public virtual bool CanTransform(Type transformType)
{
return true;
}
public bool Visit(TemplateContext context, SourceSpan span, Func<object, bool> visit)
{
if (Transformable != null)
{
return Transformable.Visit(context, span, visit);
}
return visit(Value);
}
public object Transform(TemplateContext context, SourceSpan span, Func<object, object> apply, Type destType)
{
if (Transformable != null) return Transformable.Transform(context, span, apply, destType);
return apply(Value);
}
public bool TryConvertFrom(TemplateContext context, SourceSpan span, object value)
{
if (value is IScriptTransformable valuable)
{
if (!CanTransform(valuable))
{
throw new ScriptRuntimeException(span, $"Cannot transform the value `{value}` between the element type `{valuable.ElementType.ScriptPrettyName()}` and the target function `{ElementType.ScriptPrettyName()}`.");
}
Transformable = valuable;
}
else Value = Accept(context, span, value);
return true;
}
protected virtual bool CanTransform(IScriptTransformable valuable)
{
return true;
}
protected virtual object Accept(TemplateContext context, SourceSpan span, object value)
{
return value;
}
}
public class KalkDoubleValue : KalkCompositeValue
{
public KalkDoubleValue()
{
}
public KalkDoubleValue(IScriptTransformable transformable) : base(transformable)
{
}
public KalkDoubleValue(object value) : base(value)
{
}
public override Type ElementType => typeof(double);
public override bool CanTransform(Type transformType)
{
return transformType == typeof(double) || transformType == typeof(float) || transformType == typeof(KalkHalf);
}
protected override bool CanTransform(IScriptTransformable valuable)
{
return valuable.CanTransform(typeof(double));
}
protected override object Accept(TemplateContext context, SourceSpan span, object value)
{
return context.ToObject<double>(span, value);
}
}
public class KalkTrigDoubleValue : KalkDoubleValue
{
public KalkTrigDoubleValue()
{
}
public KalkTrigDoubleValue(IScriptTransformable transformable) : base(transformable)
{
}
public KalkTrigDoubleValue(object value) : base(value)
{
}
public override Type ElementType => typeof(double);
public override bool CanTransform(Type transformType)
{
return transformType == typeof(double) || transformType == typeof(float) || transformType == typeof(KalkHalf);
}
protected override bool CanTransform(IScriptTransformable valuable)
{
return valuable.CanTransform(typeof(double));
}
protected override object Accept(TemplateContext context, SourceSpan span, object value)
{
return context.ToObject<double>(span, value);
}
}
public class KalkLongValue : KalkCompositeValue
{
public override Type ElementType => typeof(long);
public override bool CanTransform(Type transformType)
{
return transformType == typeof(long) || transformType == typeof(int);
}
protected override bool CanTransform(IScriptTransformable valuable)
{
return valuable.CanTransform(typeof(long));
}
protected override object Accept(TemplateContext context, SourceSpan span, object value)
{
return context.ToObject<long>(span, value);
}
}
public class KalkIntValue : KalkCompositeValue
{
public override Type ElementType => typeof(int);
public override bool CanTransform(Type transformType)
{
return transformType == typeof(int);
}
protected override bool CanTransform(IScriptTransformable valuable)
{
return valuable.CanTransform(typeof(int));
}
protected override object Accept(TemplateContext context, SourceSpan span, object value)
{
return context.ToObject<int>(span, value);
}
}
}

View File

@@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using Scriban.Runtime;
namespace Kalk.Core
{
public readonly struct KalkConsoleKey : IEquatable<KalkConsoleKey>
{
// Maps of name to ConsoleKey
private static readonly Dictionary<string, ConsoleKey> MapNameToConsoleKey = new Dictionary<string, ConsoleKey>(StringComparer.OrdinalIgnoreCase);
// Maps of ConsoleKey to name
private static readonly Dictionary<ConsoleKey, string> MapConsoleKeyToName = new Dictionary<ConsoleKey, string>();
static KalkConsoleKey()
{
var names = Enum.GetNames<ConsoleKey>();
var values = Enum.GetValues<ConsoleKey>();
for (var i = 0; i < names.Length; i++)
{
var name = StandardMemberRenamer.Rename(names[i]);
var key = values[i];
if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9)
{
name = ('0' + (key - ConsoleKey.D0)).ToString(CultureInfo.InvariantCulture);
}
MapKey(name, key);
}
MapKey("left", ConsoleKey.LeftArrow);
MapKey("right", ConsoleKey.RightArrow);
MapKey("up", ConsoleKey.UpArrow);
MapKey("down", ConsoleKey.DownArrow);
}
private static void MapKey(string name, ConsoleKey key)
{
MapNameToConsoleKey.Add(name, key);
MapConsoleKeyToName[key] = name;
}
public KalkConsoleKey(ConsoleModifiers modifiers, ConsoleKey key, char keyChar)
{
Modifiers = modifiers;
Key = key;
// Normalize keychar, whenever there is a modifier, we don't store the character
if (modifiers == ConsoleModifiers.Shift && key >= ConsoleKey.A && key <= ConsoleKey.Z)
{
Modifiers = 0;
KeyChar = keyChar;
}
else
{
// We need to force char to 0 when backspace, as it is not consistent between Windows and macOS/Linux
KeyChar = Modifiers != 0 || key == ConsoleKey.Backspace ? (char)0 : keyChar;
}
}
public ConsoleModifiers Modifiers { get; }
public ConsoleKey Key { get; }
public char KeyChar { get; }
public bool Equals(KalkConsoleKey other)
{
return Key == other.Key &&
KeyChar == other.KeyChar &&
Modifiers == other.Modifiers;
}
public override bool Equals(object obj)
{
return obj is KalkConsoleKey other && Equals(other);
}
public override int GetHashCode()
{
var hashCode = (int)Modifiers;
hashCode = (hashCode * 397) ^ (int) Key;
hashCode = (hashCode * 397) ^ (int) KeyChar;
return hashCode;
}
public static bool operator ==(KalkConsoleKey left, KalkConsoleKey right)
{
return left.Equals(right);
}
public static bool operator !=(KalkConsoleKey left, KalkConsoleKey right)
{
return !left.Equals(right);
}
public static KalkConsoleKey Parse(string keyText)
{
if (keyText == null) throw new ArgumentNullException(nameof(keyText));
ConsoleModifiers modifiers = 0;
ConsoleKey key = 0;
char keyChar = (char)0;
int index = 0;
bool expectingPlus = false;
while (index < keyText.Length)
{
if (expectingPlus)
{
if (keyText[index] != '+')
{
throw new ArgumentException($"Unexpected character `{keyText[index]}`. Expecting a + between keys.", nameof(keyText));
}
index++;
expectingPlus = false;
}
else
{
if (string.Compare(keyText, index, "CTRL", 0, "CTRL".Length, true) == 0)
{
if ((modifiers & ConsoleModifiers.Control) != 0) throw new ArgumentException("Cannot have multiple CTRL keys in a shortcut.", nameof(keyText));
modifiers |= ConsoleModifiers.Control;
index += "CTRL".Length;
expectingPlus = true;
}
else if (string.Compare(keyText, index, "ALT", 0, "ALT".Length, true) == 0)
{
if ((modifiers & ConsoleModifiers.Alt) != 0) throw new ArgumentException("Cannot have multiple ALT keys in a shortcut.", nameof(keyText));
modifiers |= ConsoleModifiers.Alt;
index += "ALT".Length;
expectingPlus = true;
}
else if (string.Compare(keyText, index, "SHIFT", 0, "SHIFT".Length, true) == 0)
{
// Skip the shift but don't expect it's modifiers
index += "SHIFT".Length;
expectingPlus = true;
}
else
{
var remainingText = keyText.Substring(index);
expectingPlus = false;
if (!MapNameToConsoleKey.TryGetValue(remainingText, out key))
{
throw new ArgumentException($"Invalid key found `{remainingText}`. Expecting: {string.Join(", ", Enum.GetNames(typeof(ConsoleKey)))}.", nameof(keyText));
}
if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9)
{
keyChar = (char) ('0' + (int) key - (int) ConsoleKey.D0);
}
else if (key >= ConsoleKey.A && key <= ConsoleKey.Z)
{
keyChar = remainingText[0];
}
else
{
keyChar = key switch
{
ConsoleKey.Add => '+',
ConsoleKey.Attention => '!',
ConsoleKey.Divide => '/',
ConsoleKey.Multiply => '*',
ConsoleKey.Subtract => '-',
ConsoleKey.OemPlus => '=',
ConsoleKey.OemMinus => '-',
ConsoleKey.OemPeriod => ';',
ConsoleKey.OemComma => ',',
ConsoleKey.Tab => '\t',
ConsoleKey.Enter => '\r',
ConsoleKey.Spacebar => ' ',
ConsoleKey.Backspace => (char)0,
ConsoleKey.Escape => (char) 0x1b,
_ => keyChar
};
}
break;
}
}
}
return new KalkConsoleKey(modifiers, key, keyChar);
}
public override string ToString()
{
var builder = new StringBuilder();
var modifiers = Modifiers;
if ((modifiers & ConsoleModifiers.Control) != 0)
{
builder.Append("ctrl");
}
if ((modifiers & ConsoleModifiers.Alt) != 0)
{
if (builder.Length > 0) builder.Append('+');
builder.Append("alt");
}
if ((modifiers & ConsoleModifiers.Shift) != 0)
{
if (builder.Length > 0) builder.Append('+');
builder.Append("shift");
}
if (builder.Length > 0)
{
builder.Append('+');
}
if (Key >= ConsoleKey.A && Key <= ConsoleKey.Z && KeyChar != 0)
{
builder.Append(KeyChar);
}
else
{
builder.Append(MapConsoleKeyToName[Key]);
}
return builder.ToString();
}
public static implicit operator KalkConsoleKey(ConsoleKeyInfo keyInfo)
{
return new KalkConsoleKey(keyInfo.Modifiers, keyInfo.Key, keyInfo.KeyChar);
}
public static implicit operator ConsoleKeyInfo(KalkConsoleKey key)
{
return new ConsoleKeyInfo(key.KeyChar, key.Key, (key.Modifiers & ConsoleModifiers.Shift) != 0, (key.Modifiers & ConsoleModifiers.Alt) != 0, (key.Modifiers & ConsoleModifiers.Control) != 0);
}
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Threading.Tasks;
using Scriban;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public abstract class KalkConstructor
{
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
namespace Kalk.Core
{
public class KalkDescriptor
{
public KalkDescriptor()
{
Names = new List<string>();
Params = new List<KalkParamDescriptor>();
}
public List<string> Names { get;}
public bool IsCommand { get; set; }
public string Category { get; set; }
public string Description { get; set; }
public List<KalkParamDescriptor> Params { get; }
public string Syntax { get; set; }
public string Returns { get; set; }
public string Remarks { get; set; }
public string Example { get; set; }
}
public class KalkParamDescriptor
{
public KalkParamDescriptor()
{
}
public KalkParamDescriptor(string name, string description)
{
Name = name;
Description = description;
}
public string Name { get; set; }
public string Description { get; set; }
public bool IsOptional { get; set; }
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using MathNet.Numerics.LinearAlgebra.Complex;
using Scriban;
using Scriban.Helpers;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public abstract class KalkExpression : KalkObject, IScriptCustomBinaryOperation, IScriptCustomUnaryOperation, IFormattable, IScriptTransformable
{
protected KalkExpression()
{
OriginalExpression = this;
}
public abstract object GetValue();
public abstract KalkUnit GetUnit();
public override string ToString()
{
return ToString(null, CultureInfo.CurrentCulture);
}
public abstract string ToString(string format, IFormatProvider formatProvider);
public static bool Equals(TemplateContext context, KalkExpression left, KalkExpression right)
{
if (ReferenceEquals(null, right)) return false;
if (ReferenceEquals(left, right)) return true;
return right.GetType() == left.GetType() && left.EqualsImpl(context, right);
}
public KalkExpression OriginalExpression { get; set; }
public sealed override IScriptObject Clone(bool deep)
{
throw new NotSupportedException("This expression cannot be cloned.");
}
protected abstract bool EqualsImpl(TemplateContext context, KalkExpression right);
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptBinaryOperator op, SourceSpan leftSpan, object leftValue, SourceSpan rightSpan, object rightValue, out object result)
{
var leftExpr = leftValue as KalkExpression;
if (leftExpr is null && !KalkValue.IsNumber(leftValue))
{
throw new ScriptRuntimeException(leftSpan, "Expecting a number, vector or matrix");
}
var rightExpr = rightValue as KalkExpression;
if (rightExpr is null && !KalkValue.IsNumber(rightValue))
{
throw new ScriptRuntimeException(rightSpan, "Expecting a number, vector or matrix");
}
result = null;
switch (op)
{
case ScriptBinaryOperator.CompareEqual:
case ScriptBinaryOperator.CompareNotEqual:
case ScriptBinaryOperator.CompareLessOrEqual:
case ScriptBinaryOperator.CompareGreaterOrEqual:
case ScriptBinaryOperator.CompareLess:
case ScriptBinaryOperator.CompareGreater:
if (leftExpr != null && rightExpr != null)
{
var leftSimplifier = new KalkExpressionSimplifier(context);
var (leftValueMultiplier, newLeftExpr) = leftSimplifier.Canonical(leftExpr);
var rightSimplifier = new KalkExpressionSimplifier(context);
var (rightValueMultiplier, newRightExpr) = rightSimplifier.Canonical(rightExpr);
var exprEquals = Equals(context, newLeftExpr, newRightExpr);
if (exprEquals)
{
var almostEqual = KalkValue.AlmostEqual(leftValueMultiplier, rightValueMultiplier);
switch (op)
{
case ScriptBinaryOperator.CompareEqual:
result = almostEqual;
break;
case ScriptBinaryOperator.CompareNotEqual:
result = !almostEqual;
break;
case ScriptBinaryOperator.CompareLessOrEqual:
result = almostEqual || ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValueMultiplier, rightSpan, rightValueMultiplier) is bool v1 && v1;
break;
case ScriptBinaryOperator.CompareGreaterOrEqual:
result = almostEqual || ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValueMultiplier, rightSpan, rightValueMultiplier) is bool v2 && v2;
break;
case ScriptBinaryOperator.CompareLess:
result = ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValueMultiplier, rightSpan, rightValueMultiplier) is bool v3 && v3;
break;
case ScriptBinaryOperator.CompareGreater:
result = ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValueMultiplier, rightSpan, rightValueMultiplier) is bool v4 && v4;
break;
}
}
else
{
result = op == ScriptBinaryOperator.CompareNotEqual;
}
}
else
{
if (op == ScriptBinaryOperator.CompareEqual || op == ScriptBinaryOperator.CompareNotEqual)
{
result = op == ScriptBinaryOperator.CompareNotEqual;
}
else
{
throw NotMatching(leftSpan, leftExpr, rightSpan, rightExpr);
}
}
return true;
case ScriptBinaryOperator.Multiply:
case ScriptBinaryOperator.Divide:
case ScriptBinaryOperator.Power:
{
if (op != ScriptBinaryOperator.Power || rightExpr == null)
{
var simplifier = new KalkExpressionSimplifier(context);
var (valueMul, valueExpr) = simplifier.Canonical(new KalkBinaryExpression(leftValue, op, rightValue));
var valueBinExpr = valueExpr as KalkBinaryExpression;
result = KalkValue.AlmostEqual(valueMul, 1.0) && valueBinExpr == null ? valueExpr ?? (object)1.0 : valueExpr == null ? valueMul : (object)new KalkBinaryExpression(valueMul, ScriptBinaryOperator.Multiply, valueExpr)
{
OriginalExpression = new KalkBinaryExpression(leftValue, op, rightValue)
};
return true;
}
else
{
throw new ScriptRuntimeException(rightSpan, "Cannot use a unit as an exponent.");
}
}
case ScriptBinaryOperator.Add:
case ScriptBinaryOperator.Subtract:
if (leftExpr != null && rightExpr != null)
{
var leftSimplifier = new KalkExpressionSimplifier(context);
var (leftValueMultiplier, newLeftExpr) = leftSimplifier.Canonical(leftExpr);
var rightSimplifier = new KalkExpressionSimplifier(context);
var (rightValueMultiplier, newRightExpr) = rightSimplifier.Canonical(rightExpr);
if (!Equals(context, newLeftExpr, newRightExpr))
{
throw new ScriptRuntimeException(span, $"Cannot {(op == ScriptBinaryOperator.Add ? "add" : "subtract")} the expression. Units are not matching. The left expression with unit `{newLeftExpr}` is not matching the right expression with unit `{newRightExpr}`.");
}
result = new KalkBinaryExpression(ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValueMultiplier, rightSpan, rightValueMultiplier), ScriptBinaryOperator.Multiply, newLeftExpr)
{
OriginalExpression = new KalkBinaryExpression(leftValue, op, rightValue)
};
}
else
{
throw NotMatching(leftSpan, leftExpr, rightSpan, rightExpr);
}
return true;
}
return false;
}
private static ScriptRuntimeException NotMatching(SourceSpan leftSpan, KalkExpression leftExpr, SourceSpan rightSpan, KalkExpression rightExpr)
{
return new ScriptRuntimeException(leftExpr == null ? leftSpan : rightSpan, $"Missing unit for the {(leftExpr == null ? "left" : "right")} expression. It must match the unit of the other expression `{leftExpr ?? rightExpr}`.");
}
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptUnaryOperator op, object rightValue, out object result)
{
result = null;
return false;
}
public KalkExpression ConvertTo(TemplateContext context, KalkExpression dst)
{
var src = this;
var destExpr = dst.OriginalExpression ?? dst;
var span = context.CurrentSpan;
if (!this.TryEvaluate(context, span, ScriptBinaryOperator.Divide, span, this, span, dst, out var result) || result is KalkExpression)
{
throw new ScriptRuntimeException(context.CurrentSpan, $"Cannot convert the expression {src} to the unit `{destExpr}`. Units are not matching.");
}
var value = context.ToObject<double>(span, result);
return new KalkBinaryExpression(value, ScriptBinaryOperator.Multiply, dst.OriginalExpression);
}
public Type ElementType => typeof(double);
public bool CanTransform(Type transformType)
{
return transformType == typeof(double) || transformType == typeof(float);
}
public bool Visit(TemplateContext context, SourceSpan span, Func<object, bool> visit)
{
return visit(GetValue());
}
public object Transform(TemplateContext context, SourceSpan span, Func<object, object> apply, Type destType)
{
return apply(GetValue());
}
}
}

View File

@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Scriban;
using Scriban.Syntax;
namespace Kalk.Core
{
internal class KalkExpressionSimplifier
{
private readonly TemplateContext _context;
private object _value;
private List<KalkBinaryExpression> _powers;
public KalkExpressionSimplifier(TemplateContext context)
{
_context = context;
_value = 1;
}
public (object, KalkExpression) Canonical(KalkExpression value)
{
Collect(value);
SquashPowers();
// Compute the final canonical value
KalkExpression leftValue = null;
if (_powers != null)
{
for (var i = 0; i < _powers.Count; i++)
{
var power = _powers[i];
var powerValue = _context.ToObject<double>(_context.CurrentSpan, power.Unit);
if (powerValue < 0)
{
powerValue = -powerValue;
power.Unit = powerValue;
object left = leftValue ?? (object)1.0;
leftValue = new KalkBinaryExpression(left, ScriptBinaryOperator.Divide, KalkValue.AlmostEqual(powerValue, 1.0) ? power.Value : power);
}
else
{
var nextMul = KalkValue.AlmostEqual(powerValue, 1.0) ? (KalkUnit) power.Value : (KalkExpression) power;
leftValue = leftValue == null ? nextMul : new KalkBinaryExpression(leftValue, ScriptBinaryOperator.Multiply, nextMul);
}
}
}
return (_value, leftValue);
}
private int SortPowerPerSymbol(KalkBinaryExpression left, KalkBinaryExpression right)
{
var leftSymbol = (KalkUnit) left.Value;
var rightSymbol = (KalkUnit) right.Value;
Debug.Assert(leftSymbol.Value == null);
Debug.Assert(rightSymbol.Value == null);
return string.Compare(leftSymbol.Symbol, rightSymbol.Symbol, StringComparison.Ordinal);
}
private int SortPowerPerPower(KalkBinaryExpression left, KalkBinaryExpression right)
{
var leftPowerValue = _context.ToObject<double>(_context.CurrentSpan, left.Unit);
var rightPowerValue = _context.ToObject<double>(_context.CurrentSpan, right.Unit);
var comparePower = rightPowerValue.CompareTo(leftPowerValue);
if (comparePower != 0) return comparePower;
// Then compare per symbol
return SortPowerPerSymbol(left, right);
}
private void SquashPowers()
{
if (_powers == null) return;
_powers.Sort(SortPowerPerSymbol);
// Compress the powers: a^x * a^y = a^(x+y)
KalkBinaryExpression previousPower = null;
for (var i = 0; i < _powers.Count; i++)
{
var power = _powers[i];
if (previousPower != null && ReferenceEquals(power.Value, previousPower.Value))
{
var powerResult = ScriptBinaryExpression.Evaluate(_context, _context.CurrentSpan, ScriptBinaryOperator.Add, power.Unit, previousPower.Unit);
_powers[i - 1] = new KalkBinaryExpression(power.Value, ScriptBinaryOperator.Power, powerResult);
_powers.RemoveAt(i);
i--;
continue;
}
previousPower = power;
}
// Remove any powers a^0.0
for (var i = 0; i < _powers.Count; i++)
{
var power = _powers[i];
var powerValue = _context.ToObject<double>(_context.CurrentSpan, power.Unit);
if (Math.Abs(powerValue) < (float.Epsilon * 4))
{
_powers.RemoveAt(i);
i--;
}
}
// Sort power per Power first and symbol after
// From largest to smallest
_powers.Sort(SortPowerPerPower);
}
private void Collect(KalkExpressionSimplifier simplifier)
{
Collect(simplifier._value);
if (simplifier._powers != null)
{
foreach (var power in simplifier._powers)
{
Collect(power);
}
}
}
private void Collect(object value)
{
if (value == null) return;
if (value is KalkUnit symbol)
{
if (symbol.Value != null)
{
Collect(symbol.Value);
}
else
{
if (_powers == null) _powers = new List<KalkBinaryExpression>();
_powers.Add(new KalkBinaryExpression(symbol, ScriptBinaryOperator.Power, 1));
}
}
else if (value is KalkBinaryExpression binary)
{
if (binary.Operator == ScriptBinaryOperator.Multiply)
{
var leftSimplifier = new KalkExpressionSimplifier(_context);
leftSimplifier.Collect(binary.Value);
Collect(leftSimplifier);
var rightSimplifier = new KalkExpressionSimplifier(_context);
rightSimplifier.Collect(binary.Unit);
Collect(rightSimplifier);
}
else if (binary.Operator == ScriptBinaryOperator.Divide)
{
Collect(binary.Value);
if (binary.Unit is KalkExpression)
{
Collect(new KalkBinaryExpression(binary.Unit, ScriptBinaryOperator.Power, -1));
}
else
{
var result = ScriptBinaryExpression.Evaluate(_context, _context.CurrentSpan, ScriptBinaryOperator.Divide, 1, binary.Unit);
SquashValue(result);
}
}
else
{
Debug.Assert(binary.Operator == ScriptBinaryOperator.Power);
if (_powers == null) _powers = new List<KalkBinaryExpression>();
// (a * b^2) ^ 5
// a ^ 5 * b ^ 10
var subSimplifier = new KalkExpressionSimplifier(_context);
subSimplifier.Collect(binary.Value);
var subValue = _context.ToObject<double>(_context.CurrentSpan, subSimplifier._value);
if (!KalkValue.AlmostEqual(subValue, 1.0))
{
var result = ScriptBinaryExpression.Evaluate(_context, _context.CurrentSpan, ScriptBinaryOperator.Power, subSimplifier._value, binary.Unit);
SquashValue(result);
}
if (subSimplifier._powers != null)
{
foreach (var powerExpression in subSimplifier._powers)
{
var powerResult = ScriptBinaryExpression.Evaluate(_context, _context.CurrentSpan, ScriptBinaryOperator.Multiply, powerExpression.Unit, binary.Unit);
var newPower = new KalkBinaryExpression(powerExpression.Value, ScriptBinaryOperator.Power, powerResult);
_powers.Add(newPower);
}
}
}
}
else
{
SquashValue(value);
}
}
private void SquashValue(object value)
{
_value = ScriptBinaryExpression.Evaluate(_context, _context.CurrentSpan, ScriptBinaryOperator.Multiply, _value, value);
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public abstract class KalkExpressionWithMembers : KalkExpression
{
protected abstract (string, Func<KalkExpressionWithMembers, object> getter)[] Members { get; }
public override int Count => Members.Length;
public override IEnumerable<string> GetMembers()
{
foreach (var memberPair in Members) yield return memberPair.Item1;
}
public override bool Contains(string member)
{
foreach (var memberPair in Members)
if (memberPair.Item1 == member)
return true;
return false;
}
public override bool IsReadOnly { get; set; }
public override bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
{
value = null;
foreach (var memberPair in Members)
{
if (memberPair.Item1 == member)
{
value = memberPair.Item2(this);
return true;
}
}
return false;
}
public override bool CanWrite(string member) => false;
public override bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) => false;
public override bool Remove(string member) => false;
public override void SetReadOnly(string member, bool readOnly) => throw new NotSupportedException("A member of Unit is readonly by default and cannot be modified");
}
}

View File

@@ -0,0 +1,258 @@
using System;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
/// <summary>
/// Represents a IEE-754 half-float 16-bit
/// </summary>
public readonly struct KalkHalf : IScriptCustomType, IKalkSpannable, IFormattable, IScriptTransformable, IKalkDisplayable
{
private readonly Half _value;
public KalkHalf(float value)
{
_value = (Half) value;
}
public KalkHalf(Half value)
{
_value = value;
}
public string TypeName => "half";
internal Half Value => _value;
public static float[] ToFloatValues(KalkHalf[] values)
{
if (values == null) return null;
var floatValues = new float[values.Length];
for (int i = 0; i < values.Length; i++)
{
floatValues[i] = (float)values[i];
}
return floatValues;
}
public static KalkHalf[] ToHalfValues(float[] values)
{
if (values == null) return null;
var halfValues = new KalkHalf[values.Length];
for (int i = 0; i < values.Length; i++)
{
halfValues[i] = (KalkHalf)values[i];
}
return halfValues;
}
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptBinaryOperator op, SourceSpan leftSpan, object leftValue, SourceSpan rightSpan, object rightValue, out object result)
{
result = null;
if (leftValue is KalkHalf leftHalf && rightValue is KalkHalf rightHalf)
{
result = (KalkHalf)(float)ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, (float) leftHalf, rightSpan, (float)rightHalf);
return true;
}
if (leftValue is KalkHalf leftHalf1)
{
result = (KalkHalf)(float)ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, (float)leftHalf1, rightSpan, rightValue);
return true;
}
if (rightValue is KalkHalf rightHalf1)
{
result = (KalkHalf)(float)ScriptBinaryExpression.Evaluate(context, span, op, leftSpan, leftValue, rightSpan, rightHalf1);
return true;
}
return false;
}
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptUnaryOperator op, object rightValue, out object result)
{
result = ScriptUnaryExpression.Evaluate(context, span, op, (float)(KalkHalf)rightValue);
return true;
}
public static implicit operator KalkHalf(Half half)
{
return new KalkHalf(half);
}
public static explicit operator KalkHalf(float value)
{
return new KalkHalf(value);
}
public static explicit operator float(KalkHalf half)
{
return (float)half._value;
}
public override string ToString()
{
return ToString(null, CultureInfo.InvariantCulture);
}
public void Display(KalkEngine engine, KalkDisplayMode mode)
{
switch (mode)
{
case KalkDisplayMode.Raw:
case KalkDisplayMode.Standard:
break;
case KalkDisplayMode.Developer:
DisplayDetailed(engine);
break;
}
}
private void DisplayDetailed(KalkEngine engine)
{
var u16 = Unsafe.As<Half, ushort>(ref Unsafe.AsRef(_value));
engine.WriteHighlightLine($" # IEEE 754 - half float - 16-bit");
engine.WriteHighlightLine($" #");
engine.WriteInt16(u16, true);
// Prints the 16 bits indices
engine.WriteHighlightLine($" # 15 8 0");
engine.WriteHighlightLine($" #");
engine.WriteHighlightLine($" # sign exponent |-- fraction -|");
// # 1 * 2 ^ (15 - 15) * 0b1.1000000000
var sign = (u16 >> 15);
var exponent = (u16 >> 10) & 0b11111;
var fraction = u16 & 0b1111111111;
var builder = new StringBuilder();
builder.Append(sign != 0 ? " -1" : " 1");
builder.Append(" * 2 ^ (");
builder.Append($"{(exponent == 0 ? 1 : exponent),2} - 15) * 0b{(exponent == 0 ? '0' : '1')}.");
for (int i = 9; i >= 0; i--)
{
builder.Append(((fraction >> i) & 1) == 0 ? '0' : '1');
}
engine.WriteHighlightLine($" = {builder}f");
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
return _value.ToString(format, formatProvider);
}
public Span<byte> AsSpan()
{
return MemoryMarshal.CreateSpan(ref Unsafe.As<KalkHalf, byte>(ref Unsafe.AsRef(this)), Unsafe.SizeOf<KalkHalf>());
}
public bool TryConvertTo(TemplateContext context, SourceSpan span, Type type, out object value)
{
if (type == typeof(KalkDoubleValue))
{
value = new KalkDoubleValue(this);
return true;
}
if (type == typeof(KalkTrigDoubleValue))
{
value = new KalkTrigDoubleValue(this);
return true;
}
if (type == typeof(object))
{
value = this;
return true;
}
if (type == typeof(int))
{
value = (int)(float)_value;
return true;
}
if (type == typeof(float))
{
value = (float)_value;
return true;
}
if (type == typeof(long))
{
value = (long)(float)_value;
return true;
}
if (type == typeof(double))
{
value = (double)(float)_value;
return true;
}
if (type == typeof(decimal))
{
value = (decimal) (float) _value;
return true;
}
throw new ScriptRuntimeException(span, $"Cannot convert {this} to an {type} as it has an imaginary part.");
}
public bool CanTransform(Type transformType)
{
return transformType == typeof(float) || transformType == typeof(double) || transformType == typeof(int);
}
public bool Visit(TemplateContext context, SourceSpan span, Func<object, bool> visit)
{
return false;
}
public object Transform(TemplateContext context, SourceSpan span, Func<object, object> apply, Type destType)
{
if (destType == typeof(KalkHalf))
{
return new KalkHalf((float) apply((float) _value));
}
else if (destType == typeof(float))
{
return (KalkHalf)(float) apply((float) _value);
}
else if (destType == typeof(double))
{
return (KalkHalf)(double)apply((float)_value);
}
else if (destType == typeof(int))
{
return (KalkHalf)(int)apply((float)_value);
}
return null;
}
public static KalkHalf FromObject(KalkEngine engine, SourceSpan span, object value)
{
if (value == null) return new KalkHalf();
if (value is KalkHalf half) return half;
if (value is IConvertible convertible)
{
return (KalkHalf) convertible.ToSingle(engine);
}
throw new ScriptRuntimeException(span, $"The type {engine.GetTypeName(value)} is not convertible to half");
}
public Type ElementType => typeof(float);
}
}

View File

@@ -0,0 +1,103 @@
using System.Collections;
using System.Runtime.CompilerServices;
using Kalk.Core.Helpers;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkMatrixConstructor<T> : KalkConstructor where T : unmanaged
{
public KalkMatrixConstructor(int row, int column)
{
RowCount = row;
ColumnCount = column;
}
public int RowCount { get; }
public int ColumnCount { get; }
public KalkMatrix<T> Invoke(KalkEngine context, params object[] arguments)
{
var matrix = new KalkMatrix<T>(RowCount, ColumnCount);
if (arguments.Length == 0) return matrix;
int index = 0;
int columnIndex = 0;
var maxTotal = ColumnCount * RowCount;
if (arguments.Length == 1)
{
var arg0 = arguments[0];
if (arg0 is KalkMatrix m)
{
var values = m.Values;
for (int j = 0; j < values.Length; j++)
{
matrix[index++] = context.ToObject<T>(0, values.GetValue(j));
}
}
else if (arg0 is IList list)
{
for (int j = 0; j < list.Count; j++)
{
matrix[index++] = context.ToObject<T>(0, list[j]);
}
}
else
{
var value = context.ToObject<T>(0, arg0);
for (int j = 0; j < ColumnCount * RowCount; j++)
{
matrix[index++] = value;
}
}
}
else
{
for (var i = 0; i < arguments.Length; i++)
{
var arg = arguments[i];
var argLength = arg is IList list ? list.Count : 1;
var length = columnIndex + argLength;
if (length > ColumnCount)
{
throw new ScriptArgumentException(i, $"Invalid number of arguments crossing a row. Expecting {ColumnCount} arguments instead of {length} for {matrix.TypeName}.");
}
if (length == ColumnCount)
{
columnIndex = 0;
}
else
{
columnIndex += argLength;
}
if (arg is IList listToAdd)
{
for (int j = 0; j < argLength; j++)
{
matrix[index++] = context.ToObject<T>(i, listToAdd[j]);
}
}
else
{
var value = context.ToObject<T>(i, arg);
matrix[index++] = value;
}
}
}
if (index != maxTotal)
{
throw new ScriptArgumentException(arguments.Length - 1, $"Invalid number of arguments. Expecting {maxTotal} arguments instead of {index} for {matrix.TypeName}.");
}
return matrix;
}
}
}

View File

@@ -0,0 +1,472 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Scriban;
using Scriban.Helpers;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
[DebuggerDisplay("Count={Count}")]
public unsafe class KalkNativeBuffer : IList<byte>, IList, IScriptCustomBinaryOperation, IScriptCustomTypeInfo, IKalkSpannable, IFormattable
{
private readonly SharedBuffer _buffer;
private readonly int _offset;
private readonly int _length;
public KalkNativeBuffer(int size)
{
if (size < 0) throw new ArgumentOutOfRangeException(nameof(size));
_buffer = new SharedBuffer(size);
_offset = 0;
_length = size;
}
private KalkNativeBuffer(SharedBuffer buffer, int offset, int length)
{
_buffer = buffer;
_offset = offset;
_length = length;
}
public string TypeName => "buffer";
public IntPtr GetPointer()
{
return new IntPtr(_buffer.Buffer + _offset);
}
public KalkNativeBuffer(IEnumerable<byte> buffer)
{
var array = new List<byte>(buffer).ToArray(); ;
_buffer = new SharedBuffer(array.Length);
array.CopyTo(_buffer.AsSpan());
_offset = 0;
_length = _buffer.Length;
}
public KalkNativeBuffer(byte[] buffer)
{
_buffer = new SharedBuffer(buffer.Length);
buffer.CopyTo(_buffer.AsSpan());
_offset = 0;
_length = _buffer.Length;
}
public static KalkNativeBuffer AsBytes<T>(int byteCount, in T element)
{
var buffer = new KalkNativeBuffer(byteCount);
Unsafe.CopyBlockUnaligned(ref buffer.AsSpan()[0], ref Unsafe.As<T, byte>(ref Unsafe.AsRef(element)), (uint)byteCount);
return buffer;
}
public KalkNativeBuffer Slice(int index)
{
AssertIndex(index);
return Slice(index, _length - index);
}
public KalkNativeBuffer Slice(int index, int length)
{
AssertIndex(index);
if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
if ((uint)(length + index) > (uint)_length) throw new ArgumentOutOfRangeException($"End of slice {index + length - 1} is out of range ({_length}).");
return new KalkNativeBuffer(_buffer, _offset + index, length);
}
public IEnumerator<byte> GetEnumerator()
{
for (int i = 0; i < _length; i++)
{
yield return _buffer[_offset + i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
for (int i = 0; i < _length; i++)
{
yield return _buffer[_offset + i];
}
}
public void Add(byte item) => throw new NotSupportedException("Cannot add to a fixed buffer");
int IList.Add(object? value) => throw new NotSupportedException("Cannot add to a fixed buffer");
public void Clear()
{
AsSpan().Clear();
}
bool IList.Contains(object? value) => throw new NotImplementedException();
int IList.IndexOf(object? value) => throw new NotImplementedException();
void IList.Insert(int index, object? value) => throw new NotSupportedException("Cannot add to a fixed buffer");
void IList.Remove(object? value) => throw new NotSupportedException("Cannot remove from a fixed buffer");
public bool Contains(byte item)
{
return IndexOf(item) >= 0;
}
public void CopyTo(byte[] array, int arrayIndex)
{
AsSpan().CopyTo(new Span<byte>(array, arrayIndex, array.Length - arrayIndex));
}
public bool Remove(byte item) => throw new NotSupportedException("Cannot remove from a fixed buffer");
void ICollection.CopyTo(Array array, int index)
{
for (int i = 0; i < _length; i++)
{
array.SetValue(index + i, _buffer[_offset + i]);
}
}
public int Count => _length;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot => null;
public bool IsReadOnly => false;
object? IList.this[int index]
{
get
{
AssertIndex(index);
return _buffer[_offset + index];
}
set
{
AssertIndex(index);
if (value is byte tValue)
{
this[_offset + index] = tValue;
}
else
{
this[_offset + index] = Convert.ToByte(value);
}
}
}
public int IndexOf(byte item)
{
return AsSpan().IndexOf(item);
}
public void Insert(int index, byte item) => throw new NotSupportedException("Cannot add to a fixed buffer");
public void RemoveAt(int index) => throw new NotSupportedException("Cannot remove from a fixed buffer");
bool IList.IsFixedSize => true;
public byte this[int index]
{
get
{
AssertIndex(index);
return _buffer[_offset + index];
}
set
{
AssertIndex(index);
_buffer[_offset + index] = value;
}
}
private void AssertIndex(int index)
{
if ((uint)index >= (uint)_length) throw new ArgumentOutOfRangeException($"Index {index} is out of range ({_length}).");
}
public bool TryEvaluate(TemplateContext context, SourceSpan span, ScriptBinaryOperator op, SourceSpan leftSpan, object leftValue, SourceSpan rightSpan, object rightValue, out object result)
{
result = null;
var leftArray = TryGetArray(leftValue);
var rightArray = TryGetArray(rightValue);
int intModifier = 0;
var intSpan = leftSpan;
var errorSpan = span;
string reason = null;
switch (op)
{
case ScriptBinaryOperator.BinaryOr:
case ScriptBinaryOperator.BinaryAnd:
case ScriptBinaryOperator.CompareEqual:
case ScriptBinaryOperator.CompareNotEqual:
case ScriptBinaryOperator.CompareLessOrEqual:
case ScriptBinaryOperator.CompareGreaterOrEqual:
case ScriptBinaryOperator.CompareLess:
case ScriptBinaryOperator.CompareGreater:
break;
case ScriptBinaryOperator.Multiply:
if (leftArray == null && rightArray == null || leftArray != null && rightArray != null)
{
reason = " Expecting only one array for the left or right argument.";
}
else
{
intModifier = context.ToInt(span, leftArray == null ? leftValue : rightValue);
if (rightArray == null) intSpan = rightSpan;
}
break;
case ScriptBinaryOperator.Add:
case ScriptBinaryOperator.Divide:
case ScriptBinaryOperator.DivideRound:
case ScriptBinaryOperator.Modulus:
if (leftArray == null)
{
errorSpan = leftSpan;
reason = " Expecting an array for the left argument.";
}
else
{
intModifier = context.ToInt(span, rightValue);
intSpan = rightSpan;
}
break;
case ScriptBinaryOperator.ShiftLeft:
case ScriptBinaryOperator.ShiftRight:
reason = " Cannot modify a fixed buffer.";
break;
default:
reason = String.Empty;
break;
}
if (intModifier < 0)
{
errorSpan = intSpan;
reason = $" Integer {intModifier} cannot be negative when multiplying";
}
if (reason != null)
{
throw new ScriptRuntimeException(errorSpan, $"The operator `{op.ToText()}` is not supported between {leftValue?.GetType().ScriptPrettyName()} and {rightValue?.GetType().ScriptPrettyName()}.{reason}");
}
switch (op)
{
case ScriptBinaryOperator.BinaryOr:
result = new KalkNativeBuffer(leftArray.Union(rightArray));
return true;
case ScriptBinaryOperator.BinaryAnd:
result = new KalkNativeBuffer(leftArray.Intersect(rightArray));
return true;
case ScriptBinaryOperator.Add:
try
{
result = Slice(intModifier);
}
catch (Exception ex)
{
throw new ScriptRuntimeException(errorSpan, $"Cannot shift pointer array at index {intModifier}. Reason: {ex.Message}");
}
return true;
case ScriptBinaryOperator.CompareEqual:
case ScriptBinaryOperator.CompareNotEqual:
case ScriptBinaryOperator.CompareLessOrEqual:
case ScriptBinaryOperator.CompareGreaterOrEqual:
case ScriptBinaryOperator.CompareLess:
case ScriptBinaryOperator.CompareGreater:
result = CompareTo(context, span, op, leftArray, rightArray);
return true;
case ScriptBinaryOperator.Multiply:
{
// array with integer
var array = leftArray ?? rightArray;
if (intModifier == 0)
{
result = new KalkNativeBuffer(0);
return true;
}
var newArray = new List<byte>(array);
for (int i = 0; i < intModifier; i++)
{
newArray.AddRange(array);
}
result = new KalkNativeBuffer(newArray);
return true;
}
case ScriptBinaryOperator.Divide:
case ScriptBinaryOperator.DivideRound:
{
// array with integer
var array = leftArray ?? rightArray;
if (intModifier == 0) throw new ScriptRuntimeException(intSpan, "Cannot divide by 0");
var newLength = array.Count / intModifier;
var newArray = new List<byte>(newLength);
for (int i = 0; i < newLength; i++)
{
newArray.Add(array[i]);
}
result = new KalkNativeBuffer(newArray);
return true;
}
case ScriptBinaryOperator.Modulus:
{
// array with integer
var array = leftArray ?? rightArray;
if (intModifier == 0) throw new ScriptRuntimeException(intSpan, "Cannot divide by 0");
var newArray = new List<byte>(array.Count);
for (int i = 0; i < array.Count; i++)
{
if ((i % intModifier) == 0)
{
newArray.Add(array[i]);
}
}
result = new KalkNativeBuffer(newArray);
return true;
}
}
return false;
}
private static IList<byte> TryGetArray(object rightValue)
{
var rightArray = rightValue as IList<byte>;
return rightArray;
}
private static bool CompareTo(TemplateContext context, SourceSpan span, ScriptBinaryOperator op, IList<byte> left, IList<byte> right)
{
// Compare the length first
var compare = left.Count.CompareTo(right.Count);
switch (op)
{
case ScriptBinaryOperator.CompareEqual:
if (compare != 0) return false;
break;
case ScriptBinaryOperator.CompareNotEqual:
if (compare != 0) return true;
if (left.Count == 0) return false;
break;
case ScriptBinaryOperator.CompareLessOrEqual:
case ScriptBinaryOperator.CompareLess:
if (compare < 0) return true;
if (compare > 0) return false;
if (left.Count == 0 && op == ScriptBinaryOperator.CompareLess) return false;
break;
case ScriptBinaryOperator.CompareGreaterOrEqual:
case ScriptBinaryOperator.CompareGreater:
if (compare < 0) return false;
if (compare > 0) return true;
if (left.Count == 0 && op == ScriptBinaryOperator.CompareGreater) return false;
break;
default:
throw new ScriptRuntimeException(span, $"The operator `{op.ToText()}` is not supported between {left?.GetType().ScriptPrettyName()} and {right?.GetType().ScriptPrettyName()}.");
}
// Otherwise we need to compare each element
for (int i = 0; i < left.Count; i++)
{
var result = (bool) ScriptBinaryExpression.Evaluate(context, span, op, left[i], right[i]);
if (!result)
{
return false;
}
}
return true;
}
public Span<byte> AsSpan()
{
return new Span<byte>(_buffer.Buffer + _offset, _length);
}
public override string ToString()
{
return ToString(null, null);
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
var engine = formatProvider as KalkEngine;
if (engine != null)
{
var builder = new StringBuilder();
bool isSlice = _offset > 0 || _length != this._buffer.Length;
if (isSlice)
{
builder.Append("slice(");
}
builder.Append("bytebuffer(");
builder.Append(engine.ObjectToString(new ScriptRange(this)));
builder.Append(")");
if (isSlice)
{
builder.Append($", {_offset.ToString(CultureInfo.InvariantCulture)}");
if (_offset + _length < _buffer.Length)
{
builder.Append($", {_length}");
}
builder.Append(")");
}
return builder.ToString();
}
return base.ToString();
}
private sealed class SharedBuffer
{
private const int AlignmentInBytes = 64; // 512-bits
private readonly byte* _bufferUnaligned;
public SharedBuffer(int length)
{
// We allocate enough space before and after the buffer to manage overflow
var allocatedSize = length + AlignmentInBytes * 2;
_bufferUnaligned = (byte*)Marshal.AllocHGlobal(allocatedSize);
Unsafe.InitBlockUnaligned(_bufferUnaligned, 0, (uint)allocatedSize);
if (_bufferUnaligned == null) throw new OutOfMemoryException($"Cannot allocate a buffer of {length} bytes.");
Buffer = (byte*) ((long) (_bufferUnaligned + AlignmentInBytes - 1) & ~(long) (AlignmentInBytes - 1));
Length = length;
}
~SharedBuffer()
{
Marshal.FreeHGlobal((IntPtr)_bufferUnaligned);
}
public readonly byte* Buffer;
public readonly int Length;
public ref byte this[int index] => ref Buffer[index];
public Span<byte> AsSpan() => new Span<byte>(Buffer, Length);
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
namespace Kalk.Core
{
public abstract class KalkObject : IScriptObject
{
public abstract int Count { get; }
public abstract IEnumerable<string> GetMembers();
public abstract bool Contains(string member);
public abstract bool IsReadOnly { get; set; }
public abstract bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value);
public abstract bool CanWrite(string member);
public abstract bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly);
public abstract bool Remove(string member);
public abstract void SetReadOnly(string member, bool readOnly);
public abstract IScriptObject Clone(bool deep);
public abstract string TypeName { get; }
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Text;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public sealed class KalkShortcut : ScriptObject
{
public KalkShortcut(string name, IEnumerable<KalkShortcutKeySequence> shortcuts, bool isUser)
{
if (shortcuts == null) throw new ArgumentNullException(nameof(shortcuts));
Name = name ?? throw new ArgumentNullException(nameof(name));
Keys = new ScriptArray(shortcuts) {IsReadOnly = true};
SetValue("name", Name, true);
SetValue("keys", Keys, true);
IsReadOnly = true;
IsUser = isUser;
}
public string Name { get; }
public new ScriptArray Keys { get; }
public bool IsUser { get; }
public override string ToString(string format, IFormatProvider formatProvider)
{
var builder = new StringBuilder();
builder.Append($"shortcut({Name}, ");
var mapExpressionToKeySequences = new Dictionary<ScriptExpression, List<KalkShortcutKeySequence>>();
for (var i = 0; i < Keys.Count; i++)
{
var shortcut = (KalkShortcutKeySequence)Keys[i];
if (!mapExpressionToKeySequences.TryGetValue(shortcut.Expression, out var list))
{
list = new List<KalkShortcutKeySequence>();
mapExpressionToKeySequences.Add(shortcut.Expression, list);
}
list.Add(shortcut);
}
int seqIndex = 0;
foreach (var exprToSequence in mapExpressionToKeySequences)
{
var expr = exprToSequence.Key;
var sequences = exprToSequence.Value;
if (seqIndex > 0) builder.Append(", ");
builder.Append('"');
for (var j = 0; j < sequences.Count; j++)
{
var sequence = sequences[j];
if (j > 0) builder.Append(", ");
builder.Append($"{string.Join(" ", sequence.Keys)}");
}
builder.Append($"\", {expr}");
seqIndex++;
}
builder.Append(")");
const int align = 60;
if (builder.Length < align)
{
builder.Append(new string(' ', align - builder.Length));
}
builder.Append(" # ");
for (var i = 0; i < Keys.Count; i++)
{
var shortcut = (KalkShortcutKeySequence)Keys[i];
if (i > 0) builder.Append(", ");
builder.Append($"{string.Join(" ", shortcut.Keys)} => {shortcut.Expression}");
}
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public sealed class KalkShortcutKeySequence : ScriptObject
{
public KalkShortcutKeySequence(IEnumerable<KalkConsoleKey> keys, ScriptExpression expression)
{
if (keys == null) throw new ArgumentNullException(nameof(keys));
Keys = new List<KalkConsoleKey>(keys);
Expression = expression ?? throw new ArgumentNullException(nameof(expression));
var readonlyKeys = new ScriptArray(Keys) {IsReadOnly = true};
SetValue("keys", readonlyKeys, true);
SetValue("expression", Expression.ToString(), true);
IsReadOnly = true;
}
public new List<KalkConsoleKey> Keys { get; }
public ScriptExpression Expression { get; }
public static List<KalkShortcutKeySequence> Parse(ScriptExpression keys, ScriptExpression expression)
{
if (keys == null) throw new ArgumentNullException(nameof(keys));
var keyAsString = keys is ScriptLiteral literal && literal.Value is string strValue? strValue : keys.ToString();
var keySequencesAsText = keyAsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
var keySequences = new List<KalkShortcutKeySequence>();
foreach (var keySequence in keySequencesAsText)
{
var keyList = keySequence.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var resultKeys = new List<KalkConsoleKey>();
foreach (var keyText in keyList)
{
try
{
var key = KalkConsoleKey.Parse(keyText);
resultKeys.Add(key);
}
catch (Exception ex)
{
throw new ScriptRuntimeException(keys.Span, $"Unable to parse key. Reason: {ex.Message}");
}
}
keySequences.Add(new KalkShortcutKeySequence(resultKeys, expression));
}
return keySequences;
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Scriban;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkUnit : KalkExpressionWithMembers, IScriptCustomFunction, IScriptCustomImplicitMultiplyPrecedence
{
private static readonly (string, Func<KalkExpressionWithMembers, object> getter)[] MemberDefs = {
("kind", unit => unit.TypeName),
("name", unit => ((KalkUnit)unit).Name),
("symbol", unit => ((KalkUnit)unit).Symbol),
("description", unit => ((KalkUnit)unit).Description),
("prefix", unit => ((KalkUnit)unit).Prefix),
("plural", unit => ((KalkUnit)unit).Plural),
("value", unit => ((KalkUnit)unit).Value),
};
public KalkUnit(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Symbol = name;
Plural = name + "s";
Derived = new List<KalkUnit>();
}
public override string TypeName => "symbol definition";
public string Name { get; }
public string Symbol { get; set; }
public string Description { get; set; }
public string Prefix { get; set; }
public bool IsUser { get; set; }
public string Plural { get; set; }
public KalkExpression Value { get; set; }
public List<KalkUnit> Derived { get; }
public KalkUnit Parent { get; set; }
public override object GetValue() => 1.0;
public override KalkUnit GetUnit() => this;
public override string ToString(string format, IFormatProvider formatProvider)
{
return Symbol;
}
protected override (string, Func<KalkExpressionWithMembers, object> getter)[] Members => MemberDefs;
protected override bool EqualsImpl(TemplateContext context, KalkExpression right)
{
// If we reach here, that means that the symbols are necessarily not equal
return false;
}
public int RequiredParameterCount => 0;
public int ParameterCount => 0;
public ScriptVarParamKind VarParamKind => ScriptVarParamKind.None;
public Type ReturnType => typeof(object);
public ScriptParameterInfo GetParameterInfo(int index)
{
throw new NotSupportedException("A Unit symbol does not support arguments.");
}
public virtual object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
var engine = (KalkEngine) context;
var builder = new StringBuilder();
builder.Length = 0;
builder.Append($"unit({Name}, \"{Description.Replace("\"", @"\""")}\", {Symbol}");
if (Value != null)
{
builder.Append($", {Value.OriginalExpression ?? Value}");
}
if (Prefix != null)
{
builder.Append($", prefix: \"{Prefix}\"");
}
builder.Append(")");
engine.WriteHighlightLine(builder.ToString());
if (Derived.Count > 0)
{
builder.Length = 0;
for (var i = 0; i < Derived.Count; i++)
{
var derived = Derived[i];
if (i > 0) builder.Append(", ");
builder.Append($"{derived.Name}/{derived.Symbol}");
}
engine.WriteHighlightAligned(" - ", builder.ToString());
}
return null;
}
public ValueTask<object> InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(Invoke(context, callerContext, arguments, blockStatement));
}
}
}

View File

@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
namespace Kalk.Core
{
public sealed class KalkUnitPrefix
{
private static readonly string[] PrefixNames = Enum.GetNames(typeof(KalkUnitPrefixCode));
private static readonly Array PrefixValues = Enum.GetValues(typeof(KalkUnitPrefixCode));
private static readonly KalkUnitPrefix[] DescriptorArray = new KalkUnitPrefix[PrefixNames.Length - 1];
private static readonly List<KalkUnitPrefix> Decimals = new List<KalkUnitPrefix>();
private static readonly List<KalkUnitPrefix> Binaries = new List<KalkUnitPrefix>();
private static readonly Dictionary<string, KalkUnitPrefix> MapAbbreviationToDescriptor = new Dictionary<string, KalkUnitPrefix>();
static KalkUnitPrefix()
{
for (int i = 1; i < PrefixValues.Length; i++)
{
var toMatch = (KalkUnitPrefixCode)PrefixValues.GetValue(i);
var descriptor = GetDescriptor(toMatch);
DescriptorArray[i - 1] = descriptor;
MapAbbreviationToDescriptor[descriptor.Abbreviation] = descriptor;
if (descriptor.Base == 10) Decimals.Add(descriptor);
else if (descriptor.Base == 2) Binaries.Add(descriptor);
}
}
public KalkUnitPrefix(KalkUnitPrefixCode code, int exponent)
{
Code = code;
Name = code.ToString();
Abbreviation = Name[0].ToString(CultureInfo.InvariantCulture);
Name = Name.ToLowerInvariant();
Prefix = Abbreviation;
Base = 10;
Exponent = exponent;
}
public KalkUnitPrefix(KalkUnitPrefixCode code, string abbreviation, int exponent) : this(code, abbreviation, abbreviation, 10, exponent)
{
}
public KalkUnitPrefix(KalkUnitPrefixCode code, string abbreviation, int @base, int exponent) : this(code, abbreviation, abbreviation, @base, exponent)
{
}
public KalkUnitPrefix(KalkUnitPrefixCode code, string abbreviation, string prefix, int @base, int exponent)
{
Code = code;
Name = code.ToString().ToLowerInvariant();
Abbreviation = abbreviation;
Prefix = prefix;
Base = @base;
Exponent = exponent;
}
public KalkUnitPrefixCode Code { get; }
public string Name { get; }
public string Abbreviation { get; }
public string Prefix { get; }
public int Base { get; }
public int Exponent { get; }
public override string ToString()
{
return $"{Name,10} - {Abbreviation,2} '{Prefix,2}' {Base,2}^{Exponent}";
}
public static bool TryGet(string abbreviation, out KalkUnitPrefix descriptor)
{
if (abbreviation == null) throw new ArgumentNullException(nameof(abbreviation));
return MapAbbreviationToDescriptor.TryGetValue(abbreviation, out descriptor);
}
public static IReadOnlyList<KalkUnitPrefix> GetDecimals()
{
return Decimals;
}
public static IReadOnlyList<KalkUnitPrefix> GetBinaries()
{
return Binaries;
}
public static List<KalkUnitPrefix> GetListFromCode(KalkUnitPrefixCode prefixCode)
{
var descriptors = new List<KalkUnitPrefix>();
for (int i = 0; i < PrefixValues.Length; i++)
{
var toMatch = (KalkUnitPrefixCode)PrefixValues.GetValue(i);
if ((toMatch & prefixCode) != 0)
{
descriptors.Add(DescriptorArray[i]);
}
}
return descriptors;
}
private static KalkUnitPrefix GetDescriptor(KalkUnitPrefixCode singlePrefixCode)
{
switch (singlePrefixCode)
{
case KalkUnitPrefixCode.Zetta:
return new KalkUnitPrefix(KalkUnitPrefixCode.Zetta, 24);
case KalkUnitPrefixCode.Yotta:
return new KalkUnitPrefix(KalkUnitPrefixCode.Yotta, 21);
case KalkUnitPrefixCode.Exa:
return new KalkUnitPrefix(KalkUnitPrefixCode.Exa, 18);
case KalkUnitPrefixCode.Peta:
return new KalkUnitPrefix(KalkUnitPrefixCode.Peta, 15);
case KalkUnitPrefixCode.Tera:
return new KalkUnitPrefix(KalkUnitPrefixCode.Tera, 12);
case KalkUnitPrefixCode.Giga:
return new KalkUnitPrefix(KalkUnitPrefixCode.Giga, 9);
case KalkUnitPrefixCode.Mega:
return new KalkUnitPrefix(KalkUnitPrefixCode.Mega, 6);
case KalkUnitPrefixCode.kilo:
return new KalkUnitPrefix(KalkUnitPrefixCode.kilo, 3);
case KalkUnitPrefixCode.hecto:
return new KalkUnitPrefix(KalkUnitPrefixCode.hecto, 2);
case KalkUnitPrefixCode.deca:
return new KalkUnitPrefix(KalkUnitPrefixCode.deca, "da", 10, 1);
case KalkUnitPrefixCode.deci:
return new KalkUnitPrefix(KalkUnitPrefixCode.deci, -1);
case KalkUnitPrefixCode.centi:
return new KalkUnitPrefix(KalkUnitPrefixCode.centi, -2);
case KalkUnitPrefixCode.milli:
return new KalkUnitPrefix(KalkUnitPrefixCode.milli, -3);
case KalkUnitPrefixCode.micro:
return new KalkUnitPrefix(KalkUnitPrefixCode.micro, "µ", -6);
case KalkUnitPrefixCode.nano:
return new KalkUnitPrefix(KalkUnitPrefixCode.nano, -9);
case KalkUnitPrefixCode.pico:
return new KalkUnitPrefix(KalkUnitPrefixCode.pico, -12);
case KalkUnitPrefixCode.femto:
return new KalkUnitPrefix(KalkUnitPrefixCode.femto, -15);
case KalkUnitPrefixCode.atto:
return new KalkUnitPrefix(KalkUnitPrefixCode.atto, -18);
case KalkUnitPrefixCode.zepto:
return new KalkUnitPrefix(KalkUnitPrefixCode.zepto, -21);
case KalkUnitPrefixCode.yocto:
return new KalkUnitPrefix(KalkUnitPrefixCode.yocto, -24);
case KalkUnitPrefixCode.Kibi: // - Ki 2^10 kibibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Kibi, "Ki", 2, 10);
case KalkUnitPrefixCode.Mibi: // - Mi 2^20 mibibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Mibi, "Mi", 2, 20);
case KalkUnitPrefixCode.Gibi: // - Gi 2^30 gibibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Gibi, "Gi", 2, 30);
case KalkUnitPrefixCode.Tibi: // - Ti 2^40 tebibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Tibi, "Ti", 2, 40);
case KalkUnitPrefixCode.Pibi: // - Pi 2^50 pebibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Pibi, "Pi", 2, 50);
case KalkUnitPrefixCode.Eibi: // - Ei 2^60 exbibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Eibi, "Ei", 2, 60);
case KalkUnitPrefixCode.Zibi: // - Zi 2^70 zebibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Zibi, "Zi", 2, 70);
case KalkUnitPrefixCode.Yibi: // - Yi 2^80 yobibit
return new KalkUnitPrefix(KalkUnitPrefixCode.Yibi, "Yi", 2, 80);
//case KalkUnitPrefixCode.Kbit: // - Kb 2^10 kilobit
// return new KalkUnitPrefix(KalkUnitPrefixCode.Kbit, "Kb", "K", 2, 10);
//case KalkUnitPrefixCode.Mbit: // - Mb 2^20 megabit
// return new KalkUnitPrefix(KalkUnitPrefixCode.Mbit, "Mb", "M", 2, 20);
//case KalkUnitPrefixCode.Gbi: // - Gb 2^30 gigabit
// return new KalkUnitPrefix(KalkUnitPrefixCode.Gbi, "Gb", "G", 2, 30);
default:
throw new ArgumentOutOfRangeException(nameof(singlePrefixCode), singlePrefixCode, null);
}
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using System.ComponentModel.Design;
using System.Linq;
namespace Kalk.Core
{
[Flags]
public enum KalkUnitPrefixCode
{
None = 0,
// Decimal
Yotta ,// - Y 10^24
Zetta,// - Z 10^21
Exa, // - E 10^18
Peta, // - P 10^15
Tera, // - T 10^12
Giga, // - G 10^9
Mega, // - M 10^6
kilo, // - k 10^3
hecto, // - h 10^2
deca, // - da 10^1
deci, // - d 10^-1
centi, // - c 10^-2
milli, // - m 10^-3
micro, // - µ 10^-6
nano, // - n 10^-9
pico, // - p 10^-12
femto, // - f 10^-15
atto, // - a 10^-18
zepto, // - z 10^-21
yocto, // - y 10^-24
// Binary
Kibi, // - Ki 2^10 kibibit
Mibi, // - Mi 2^20 mibibit
Gibi, // - Gi 2^30 gibibit
Tibi, // - Ti 2^40 tebibit
Pibi, // - Pi 2^50 pebibit
Eibi, // - Ei 2^60 exbibit
Zibi, // - Zi 2^70 zebibit
Yibi, // - Yi 2^80 yobibit
//Kbit, // - Kb 2^10 kilobit
//Mbit, // - Mb 2^20 megabit
//Gbi, // - Gb 2^30 gigabit
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using MathNet.Numerics;
using Scriban;
using Scriban.Helpers;
using Scriban.Parsing;
using Scriban.Runtime;
namespace Kalk.Core
{
public interface IKalkSpannable
{
Span<byte> AsSpan();
}
public abstract class KalkValue : KalkObject, IScriptTransformable, IKalkSpannable
{
public abstract Type ElementType { get; }
public abstract bool CanTransform(Type transformType);
public abstract Span<byte> AsSpan();
public void BitCastFrom(Span<byte> bytes)
{
var destByte = AsSpan();
var minLength = Math.Min(destByte.Length, bytes.Length);
Unsafe.CopyBlockUnaligned(ref destByte[0], ref bytes[0], (uint)minLength);
}
public abstract bool Visit(TemplateContext context, SourceSpan span, Func<object, bool> visit);
public abstract object Transform(TemplateContext context, SourceSpan span, Func<object, object> apply, Type destType);
public static bool IsNumber(object value)
{
return value is KalkValue || (value != null && value.GetType().IsNumber());
}
public static bool AlmostEqual(object left, object right)
{
if (!IsNumber(left)) throw new ArgumentOutOfRangeException(nameof(left), $"The value `{left}` is not a number");
if (!IsNumber(right)) throw new ArgumentOutOfRangeException(nameof(left), $"The value `{right}` is not a number");
var leftDouble = Convert.ToDouble(left);
var rightDouble = Convert.ToDouble(right);
return leftDouble.AlmostEqual(rightDouble);
}
public static bool AlmostEqual(double left, double right)
{
return left.AlmostEqual(right);
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace Kalk.Core.Modules
{
/// <summary>
/// Import all modules.
/// </summary>
[KalkExportModule("All")]
public partial class AllModule : KalkModule
{
public AllModule()
{
Modules = new List<KalkModule>();
RegisterDocumentationAuto();
}
public List<KalkModule> Modules { get; }
protected override void Import()
{
base.Import();
foreach (var module in Modules)
{
module.InternalImport();
}
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using Kalk.Core.Helpers;
using Scriban.Runtime;
namespace Kalk.Core.Modules
{
/// <summary>
/// Module for loading/parsing CSV text/files.
/// </summary>
[KalkExportModule(ModuleName)]
public partial class CsvModule : KalkModuleWithFunctions
{
private const string ModuleName = "Csv";
public CsvModule() : base(ModuleName)
{
RegisterFunctionsAuto();
}
/// <summary>
/// Parse the specified text as a CSV, returning each CSV line in an array.
/// </summary>
/// <param name="text">The text to parse.</param>
/// <param name="headers"><c>true</c> if the text to parse has CSV headers. Default is fault.</param>
/// <returns>An array of CSV columns values.</returns>
/// <example>
/// ```kalk
/// >>> items = parse_csv("a,b,c\n1,2,3\n4,5,6\n")
/// # items = parse_csv("a,b,c\n1,2,3\n4,5,6\n")
/// items = [[1, 2, 3], [4, 5, 6]]
/// >>> items[0].a
/// # items[0].a
/// out = 1
/// >>> items[0].b
/// # items[0].b
/// out = 2
/// >>> items[0].c
/// # items[0].c
/// out = 3
/// ```
/// </example>
[KalkExport("parse_csv", StringModule.CategoryString)]
public ScriptRange ParseCsv(string text, bool headers = true)
{
if (text == null) throw new ArgumentNullException(nameof(text));
return new ScriptRange(new KalkCsvReader(() => new StringReader(text), headers));
}
/// <summary>
/// Loads the specified file as a CSV, returning each CSV line in an array.
/// </summary>
/// <param name="path">The file path to load and parse as CSV.</param>
/// <param name="headers"><c>true</c> if the file to parse has CSV headers. Default is fault.</param>
/// <returns>An array of CSV columns values.</returns>
/// <example>
/// ```kalk
/// >>> items = load_csv("test.csv")
/// # items = load_csv("test.csv")
/// items = [[1, 2, 3], [4, 5, 6]]
/// >>> items[0].a
/// # items[0].a
/// out = 1
/// >>> items[1].a
/// # items[1].a
/// out = 4
/// >>> items[1].c
/// # items[1].c
/// out = 6
/// ```
/// </example>
[KalkExport("load_csv", FileModule.CategoryMiscFile)]
public ScriptRange LoadCsv(string path, bool headers = true)
{
var fullPath = Engine.FileModule.AssertReadFile(path);
return new ScriptRange(new KalkCsvReader(() =>
{
var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new StreamReader(stream);
}, headers));
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Kalk.Core.Modules;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkCurrencies : IScriptObject, IScriptCustomFunction
{
private KalkUnits _units;
private readonly CurrencyModule _currencyModule;
public KalkCurrencies(CurrencyModule currencyModule)
{
_currencyModule = currencyModule;
}
public void Initialize(KalkEngine engine)
{
if (engine == null) throw new ArgumentNullException(nameof(engine));
_units = engine.Units;
}
public int Count
{
get
{
int count = 0;
foreach (var unitPair in _units)
{
if (unitPair.Value is KalkCurrency) count++;
}
return count;
}
}
public void Clear()
{
var keys = _units.Keys.ToList();
foreach (var unitKey in keys)
{
if (_units.TryGetValue(unitKey, out var unitObject) && unitObject is KalkCurrency)
{
_units.Remove(unitKey);
}
}
}
public IEnumerable<string> GetMembers()
{
// Show only currencies from units
foreach (var unitPair in _units)
{
if (unitPair.Value is KalkCurrency) yield return unitPair.Key;
}
}
public bool Contains(string member)
{
return _units.Contains(member) && _units[member] is KalkCurrency;
}
public bool IsReadOnly
{
get => false;
set => _units.IsReadOnly = value;
}
public bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object value)
{
if (_units.TryGetValue(context, span, member, out value) && value is KalkCurrency)
{
return true;
}
value = null;
return false;
}
public bool CanWrite(string member) => false;
public bool TrySetValue(TemplateContext context, SourceSpan span, string member, object value, bool readOnly) => false;
public bool Remove(string member) => false;
public void SetReadOnly(string member, bool readOnly)
{
}
public IScriptObject Clone(bool deep)
{
throw new NotSupportedException("Clone is not supported");
}
public int RequiredParameterCount => 0;
public int ParameterCount => 0;
public ScriptVarParamKind VarParamKind => ScriptVarParamKind.None;
public Type ReturnType => typeof(object);
public ScriptParameterInfo GetParameterInfo(int index)
{
throw new NotSupportedException("Currencies don't have any parameters.");
}
public object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
var engine = (KalkEngine) context;
if (_units.All(x => x.Value.GetType() != typeof(KalkCurrency)))
{
engine.WriteHighlightLine($"# No Currencies defined (e.g try `import {nameof(CurrencyModule)}`)");
}
else
{
_units.Display(engine, $"Builtin Currencies (Last Update: {_currencyModule.LastUpdate.ToString("dd MMM yyyy", CultureInfo.InvariantCulture)})", symbol => symbol is KalkCurrency && !symbol.IsUser, false);
_units.Display(engine, "User Defined Currencies", symbol => symbol is KalkCurrency && symbol.IsUser, false);
}
return null;
}
public ValueTask<object> InvokeAsync(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
return new ValueTask<object>(Invoke(context, callerContext, arguments, blockStatement));
}
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Kalk.Core.Modules;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkCurrency : KalkUnit
{
private readonly CurrencyModule _currencyModule;
private const int CurrencyColumnAlign = 27;
public KalkCurrency(CurrencyModule currencyModule, string name) : base(name)
{
_currencyModule = currencyModule;
Plural = name;
}
public override string TypeName => "currency";
public override object GetValue() => 1.0m;
public override object Invoke(TemplateContext context, ScriptNode callerContext, ScriptArray arguments, ScriptBlockStatement blockStatement)
{
if (!(callerContext.Parent is ScriptExpressionStatement))
{
return this;
}
var engine = (KalkEngine)context;
string currencyCmd;
string currencyDesc;
if (Value == null)
{
currencyCmd = $"currency({Name});";
currencyDesc = $"Base currency";
}
else
{
var value = (KalkBinaryExpression) Value;
var valueToBase = 1.0m / (decimal) value.Value;
var format = "#0.0###";
if (valueToBase < 0.0001m)
{
format = null;
}
var formattedNumber = valueToBase.ToString(format, CultureInfo.InvariantCulture);
currencyCmd = $"currency({Name}, {formattedNumber});";
currencyDesc = $"{formattedNumber,-8} {Name} => 1 {_currencyModule.GetSafeBaseCurrencyFromConfig().Name}";
}
engine.WriteHighlightLine($"{currencyCmd, -CurrencyColumnAlign} # {currencyDesc}");
return null;
}
public static void CheckValid(SourceSpan span, string name)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (name.Length != 3) throw new ScriptRuntimeException(span, $"Base currency `{name}` must 3 characters long instead of {name.Length}.");
foreach (var c in name)
{
if (!(c >= 'A' && c <= 'Z'))
{
throw new ScriptRuntimeException(span, $"The character `{c}` is invalid for the base currency `{name}`. Only A-Z are allowed");
}
}
}
}
}

View File

@@ -0,0 +1,497 @@
using System;
using System.Collections;
using System.IO;
using System.Text;
using Kalk.Core.Helpers;
using Scriban.Runtime;
namespace Kalk.Core.Modules
{
/// <summary>
/// Modules providing file related functions.
/// </summary>
[KalkExportModule(ModuleName)]
public partial class FileModule : KalkModuleWithFunctions
{
private const string ModuleName = "Files";
public const string CategoryMiscFile = "Misc File Functions";
public FileModule() : base(ModuleName)
{
RegisterFunctionsAuto();
}
/// <summary>
/// Gets the current directory.
/// </summary>
/// <returns>The current directory.</returns>
/// <example>
/// ```kalk
/// >>> pwd
/// # pwd
/// out = "/code/kalk/tests"
/// ```
/// </example>
[KalkExport("pwd", CategoryMiscFile)]
public string CurrentDirectory()
{
return Environment.CurrentDirectory;
}
/// <summary>
/// Changes the current directory to the specified path.
/// </summary>
/// <param name="path">Path to the directory to change.</param>
/// <returns>The current directory or throws an exception if the directory does not exists</returns>
/// <example>
/// ```kalk
/// >>> cd
/// # cd
/// out = "/code/kalk/tests"
/// >>> mkdir "testdir"
/// >>> cd "testdir"
/// # cd("testdir")
/// out = "/code/kalk/tests/testdir"
/// >>> cd ".."
/// # cd("..")
/// out = "/code/kalk/tests"
/// >>> rmdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = false
/// ```
/// </example>
[KalkExport("cd", CategoryMiscFile)]
public string ChangeDirectory(string path = null)
{
if (path != null)
{
if (!DirectoryExists(path)) throw new ArgumentException($"The folder `{path}` does not exists", nameof(path));
Environment.CurrentDirectory = Path.Combine(Environment.CurrentDirectory, path);
}
return CurrentDirectory();
}
/// <summary>
/// Checks if the specified file path exists on the disk.
/// </summary>
/// <param name="path">Path to a file.</param>
/// <returns>`true` if the specified file path exists on the disk.</returns>
/// <example>
/// ```kalk
/// >>> rm "test.txt"
/// >>> file_exists "test.txt"
/// # file_exists("test.txt")
/// out = false
/// >>> save_text("content", "test.txt")
/// >>> file_exists "test.txt"
/// # file_exists("test.txt")
/// out = true
/// ```
/// </example>
[KalkExport("file_exists", CategoryMiscFile)]
public KalkBool FileExists(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
return Engine.FileService.FileExists(Path.Combine(Environment.CurrentDirectory, path));
}
/// <summary>
/// Checks if the specified directory path exists on the disk.
/// </summary>
/// <param name="path">Path to a directory.</param>
/// <returns>`true` if the specified directory path exists on the disk.</returns>
/// <example>
/// ```kalk
/// >>> mkdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = true
/// >>> rmdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = false
/// ```
/// </example>
[KalkExport("dir_exists", CategoryMiscFile)]
public KalkBool DirectoryExists(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
return Engine.FileService.DirectoryExists(Path.Combine(Environment.CurrentDirectory, path));
}
/// <summary>
/// List files and directories from the specified path or the current directory.
/// </summary>
/// <param name="path">The specified directory or the current directory if not specified.</param>
/// <param name="recursive">A boolean to perform a recursive list. Default is `false`.</param>
/// <returns>An enumeration of the files and directories.</returns>
/// <example>
/// ```kalk
/// >>> mkdir "testdir"
/// >>> cd "testdir"
/// # cd("testdir")
/// out = "/code/kalk/tests/testdir"
/// >>> mkdir "subdir"
/// >>> save_text("content", "file.txt")
/// >>> dir "."
/// # dir(".")
/// out = ["./file.txt", "./subdir"]
/// >>> save_text("content", "subdir/file2.txt")
/// >>> dir(".", true)
/// # dir(".", true)
/// out = ["./file.txt", "./subdir", "./subdir/file2.txt"]
/// >>> cd ".."
/// # cd("..")
/// out = "/code/kalk/tests"
/// >>> rmdir("testdir", true)
/// ```
/// </example>
[KalkExport("dir", CategoryMiscFile)]
public IEnumerable DirectoryListing(string path = null, bool recursive = false)
{
string search = "*";
if (!string.IsNullOrEmpty(path))
{
var wildcardIndex = path.IndexOf('*', StringComparison.OrdinalIgnoreCase);
if (wildcardIndex >= 0)
{
search = path.Substring(wildcardIndex);
path = path.Substring(0, wildcardIndex);
}
}
var fullDir = string.IsNullOrEmpty(path) ? Environment.CurrentDirectory : Path.Combine(Environment.CurrentDirectory, path);
if (!Engine.FileService.DirectoryExists(fullDir)) throw new ArgumentException($"Directory `{fullDir}` not found.");
if (string.IsNullOrEmpty(path))
{
path = ".";
}
return new ScriptRange(Engine.FileService.EnumerateFileSystemEntries(path, search, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly));
}
/// <summary>
/// Deletes a file from the specified path.
/// </summary>
/// <param name="path">Path to the file to delete.</param>
/// <example>
/// ```kalk
/// >>> rm "test.txt"
/// >>> file_exists "test.txt"
/// # file_exists("test.txt")
/// out = false
/// >>> save_text("content", "test.txt")
/// >>> file_exists "test.txt"
/// # file_exists("test.txt")
/// out = true
/// ```
/// </example>
[KalkExport("rm", CategoryMiscFile)]
public void RemoveFile(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (FileExists(path))
{
Engine.FileService.FileDelete(Path.Combine(Environment.CurrentDirectory, path));
}
}
/// <summary>
/// Creates a directory at the specified path.
/// </summary>
/// <param name="path">Path of the directory to create.</param>
/// <example>
/// ```kalk
/// >>> mkdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = true
/// >>> rmdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = false
/// ```
/// </example>
[KalkExport("mkdir", CategoryMiscFile)]
public void CreateDirectory(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (DirectoryExists(path)) return;
Engine.FileService.DirectoryCreate(Path.Combine(Environment.CurrentDirectory, path));
}
/// <summary>
/// Deletes the directory at the specified path.
/// </summary>
/// <param name="path">Path to the directory to delete.</param>
/// <example>
/// ```kalk
/// >>> mkdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = true
/// >>> rmdir "testdir"
/// >>> dir_exists "testdir"
/// # dir_exists("testdir")
/// out = false
/// ```
/// </example>
[KalkExport("rmdir", CategoryMiscFile)]
public void RemoveDirectory(string path, bool recursive = true)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (DirectoryExists(path))
{
Engine.FileService.DirectoryDelete(Path.Combine(Environment.CurrentDirectory, path), recursive);
}
}
/// <summary>
/// Loads the specified file as text.
/// </summary>
/// <param name="path">Path to a file to load as text.</param>
/// <param name="encoding">The encoding of the file. Default is "utf-8"</param>
/// <returns>The file loaded as a string.</returns>
/// <example>
/// ```kalk
/// >>> load_text "test.csv"
/// # load_text("test.csv")
/// out = "a,b,c\n1,2,3\n4,5,6"
/// ```
/// </example>
[KalkExport("load_text", CategoryMiscFile)]
public string LoadText(string path, string encoding = KalkConfig.DefaultEncoding)
{
var fullPath = AssertReadFile(path);
var encoder = GetEncoding(encoding);
using var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new StreamReader(stream, encoder).ReadToEnd();
}
/// <summary>
/// Loads the specified file as binary.
/// </summary>
/// <param name="path">Path to a file to load as binary.</param>
/// <returns>The file loaded as a a byte buffer.</returns>
/// <example>
/// ```kalk
/// >>> load_bytes "test.csv"
/// # load_bytes("test.csv")
/// out = bytebuffer([97, 44, 98, 44, 99, 10, 49, 44, 50, 44, 51, 10, 52, 44, 53, 44, 54])
/// >>> ascii out
/// # ascii(out)
/// out = "a,b,c\n1,2,3\n4,5,6"
/// ```
/// </example>
[KalkExport("load_bytes", CategoryMiscFile)]
public KalkNativeBuffer LoadBytes(string path)
{
var fullPath = AssertReadFile(path);
using var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var buffer = new KalkNativeBuffer((int)stream.Length);
stream.Read(buffer.AsSpan());
return buffer;
}
/// <summary>
/// Load each lines from the specified file path.
/// </summary>
/// <param name="path">Path to a file to load lines from.</param>
/// <param name="encoding">The encoding of the file. Default is "utf-8"</param>
/// <returns>An enumeration on the lines.</returns>
/// <example>
/// ```kalk
/// >>> load_lines "test.csv"
/// # load_lines("test.csv")
/// out = ["a,b,c", "1,2,3", "4,5,6"]
/// ```
/// </example>
[KalkExport("load_lines", CategoryMiscFile)]
public ScriptRange LoadLines(string path, string encoding = KalkConfig.DefaultEncoding)
{
var fullPath = AssertReadFile(path);
var encoder = GetEncoding(encoding);
return new ScriptRange(new LineReader(() =>
{
var stream = Engine.FileService.FileOpen(fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new StreamReader(stream, encoder);
}));
}
/// <summary>
/// Saves an array of data as string to the specified files.
/// </summary>
/// <param name="lines">An array of data.</param>
/// <param name="path">Path to the file to save the lines to.</param>
/// <param name="encoding">The encoding of the file. Default is "utf-8"</param>
/// <example>
/// ```kalk
/// >>> save_lines(1..10, "lines.txt")
/// >>> load_lines("lines.txt")
/// # load_lines("lines.txt")
/// out = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
/// ```
/// </example>
[KalkExport("save_lines", CategoryMiscFile)]
public object SaveLines(IEnumerable lines, string path, string encoding = KalkConfig.DefaultEncoding)
{
if (lines == null) throw new ArgumentNullException(nameof(lines));
var fullPath = AssertWriteFile(path);
var encoder = GetEncoding(encoding);
using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write);
using var writer = new StreamWriter(stream, encoder);
foreach (var lineObj in lines)
{
var line = Engine.ObjectToString(lineObj);
writer.WriteLine(line);
}
// return object to allow this function to be used in a pipe
return null;
}
/// <summary>
/// Saves a text to the specified file path.
/// </summary>
/// <param name="text">The text to save.</param>
/// <param name="path">Path to the file to save the text to.</param>
/// <param name="encoding">The encoding of the file. Default is "utf-8"</param>
/// <example>
/// ```kalk
/// >>> save_text("Hello World!", "test.txt")
/// >>> load_text("test.txt")
/// # load_text("test.txt")
/// out = "Hello World!"
/// ```
/// </example>
[KalkExport("save_text", CategoryMiscFile)]
public object SaveText(string text, string path, string encoding = KalkConfig.DefaultEncoding)
{
var fullPath = AssertWriteFile(path);
var encoder = GetEncoding(encoding);
using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write);
using var writer = new StreamWriter(stream, encoder);
text ??= string.Empty;
writer.Write(text);
// return object to allow this function to be used in a pipe
return null;
}
/// <summary>
/// Saves a byte buffer to the specified file path.
/// </summary>
/// <param name="data">The data to save.</param>
/// <param name="path">Path to the file to save the data to.</param>
/// <example>
/// ```kalk
/// >>> utf8("Hello World!")
/// # utf8("Hello World!")
/// out = bytebuffer([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])
/// >>> save_bytes(out, "test.bin")
/// >>> load_bytes("test.bin")
/// # load_bytes("test.bin")
/// out = bytebuffer([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33])
/// >>> utf8(out)
/// # utf8(out)
/// out = "Hello World!"
/// ```
/// </example>
[KalkExport("save_bytes", CategoryMiscFile)]
public object SaveBytes(IEnumerable data, string path)
{
var fullPath = AssertWriteFile(path);
using var stream = Engine.FileService.FileOpen(fullPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write);
switch (data)
{
case ScriptRange range when range.Values is byte[] byteBuffer:
stream.Write(byteBuffer, 0, byteBuffer.Length);
break;
case KalkNativeBuffer byteBuffer:
{
stream.Write(byteBuffer.AsSpan());
break;
}
case ScriptArray<byte> scriptByteArray:
for (int i = 0; i < scriptByteArray.Count; i++)
{
stream.WriteByte(scriptByteArray[i]);
}
break;
default:
{
if (data != null)
{
foreach (var item in data)
{
var b = Engine.ToObject<byte>(0, item);
stream.WriteByte(b);
}
}
break;
}
}
// return object to allow this function to be used in a pipe
return null;
}
public string AssertWriteFile(string path)
{
return AssertFile(path, true);
}
public string AssertReadFile(string path)
{
return AssertFile(path, false);
}
private string AssertFile(string path, bool toWrite)
{
if (path == null) throw new ArgumentNullException(nameof(path));
var fullPath = Path.Combine(Environment.CurrentDirectory, path);
if (toWrite)
{
var directory = Path.GetDirectoryName(fullPath);
if (!Engine.FileService.DirectoryExists(directory))
{
try
{
Engine.FileService.DirectoryCreate(directory);
}
catch (Exception ex)
{
throw new ArgumentException($"Cannot write to file {path}. Cannot create directory {directory}. Reason: {ex.Message}");
}
}
}
else
{
if (!Engine.FileService.FileExists(fullPath)) throw new ArgumentException($"File {path} was not found.");
}
return fullPath;
}
private static Encoding GetEncoding(string encoding)
{
if (encoding == null) throw new ArgumentNullException(nameof(encoding));
try
{
switch (encoding)
{
case "utf-8": return Encoding.UTF8;
case "utf-32": return Encoding.UTF32;
case "ascii": return Encoding.ASCII;
default:
return Encoding.GetEncoding(encoding);
}
}
catch (Exception ex)
{
throw new ArgumentException($"Invalid encoding name `{encoding}`. Reason: {ex.Message}");
}
}
}
}

View File

@@ -0,0 +1,37 @@
namespace Kalk.Core.Modules.HardwareIntrinsics.Arm
{
public partial class AdvSimdIntrinsicsModule : IntrinsicsModuleBase
{
public AdvSimdIntrinsicsModule() : base("ARM") => RegisterFunctionsAuto();
}
public partial class AdvSimdArm64IntrinsicsModule : IntrinsicsModuleBase
{
public AdvSimdArm64IntrinsicsModule() : base("ARM64") => RegisterFunctionsAuto();
}
public partial class AesIntrinsicsModule : IntrinsicsModuleBase
{
public AesIntrinsicsModule() : base("ARM AES") => RegisterFunctionsAuto();
}
public partial class Crc32IntrinsicsModule : IntrinsicsModuleBase
{
public Crc32IntrinsicsModule() : base("ARM Crc32") => RegisterFunctionsAuto();
}
public partial class Crc32Arm64IntrinsicsModule : IntrinsicsModuleBase
{
public Crc32Arm64IntrinsicsModule() : base("ARM64 Crc32") => RegisterFunctionsAuto();
}
public partial class Sha1IntrinsicsModule : IntrinsicsModuleBase
{
public Sha1IntrinsicsModule() : base("ARM Sha1") => RegisterFunctionsAuto();
}
public partial class Sha256IntrinsicsModule : IntrinsicsModuleBase
{
public Sha256IntrinsicsModule() : base("ARM Sha256") => RegisterFunctionsAuto();
}
}

View File

@@ -0,0 +1,127 @@
using Kalk.Core.Modules.HardwareIntrinsics;
namespace Kalk.Core.Modules
{
/// <summary>
/// Module with CPU Hardware intrinsics.
/// </summary>
[KalkExportModule(ModuleName)]
public partial class HardwareIntrinsicsModule : KalkModuleWithFunctions
{
private const string ModuleName = "HardwareIntrinsics";
private const string CategoryIntrinsics = "Vector Hardware Intrinsics";
public HardwareIntrinsicsModule() : base(ModuleName)
{
RegisterDocumentationAuto();
}
protected override void Import()
{
// X86
if (System.Runtime.Intrinsics.X86.Sse.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.SseIntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.SseX64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse2.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse2IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse2.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse2X64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse3.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse3IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Ssse3.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Ssse3IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse41.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse41IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse41.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse41X64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse42.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse42IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Sse42.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Sse42X64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Avx.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.AvxIntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Avx2.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Avx2IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Aes.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.AesIntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Bmi1.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Bmi1IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Bmi1.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Bmi1X64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Bmi2.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Bmi2IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.X86.Bmi2.X64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.X86.Bmi2X64IntrinsicsModule>();
}
// Arm
if (System.Runtime.Intrinsics.Arm.AdvSimd.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.AdvSimdIntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.AdvSimd.Arm64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.AdvSimdArm64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.Aes.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.AesIntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.Crc32.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.Crc32IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.Crc32.Arm64.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.Crc32Arm64IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.Sha1.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.Sha1IntrinsicsModule>();
}
if (System.Runtime.Intrinsics.Arm.Sha256.IsSupported)
{
DynamicRegister<HardwareIntrinsics.Arm.Sha256IntrinsicsModule>();
}
}
private void DynamicRegister<TModule>() where TModule : IntrinsicsModuleBase, new()
{
var module = new TModule();
module.Initialize(Engine);
module.InternalImport();
}
}
}

View File

@@ -0,0 +1,85 @@
namespace Kalk.Core.Modules.HardwareIntrinsics.X86
{
public partial class SseIntrinsicsModule : IntrinsicsModuleBase
{
public SseIntrinsicsModule() : base("SSE") => RegisterFunctionsAuto();
}
public partial class SseX64IntrinsicsModule : IntrinsicsModuleBase
{
public SseX64IntrinsicsModule() : base("SSE (x64)") => RegisterFunctionsAuto();
}
public partial class Sse2IntrinsicsModule : IntrinsicsModuleBase
{
public Sse2IntrinsicsModule() : base("SSE2") => RegisterFunctionsAuto();
}
public partial class Sse2X64IntrinsicsModule : IntrinsicsModuleBase
{
public Sse2X64IntrinsicsModule() : base("SSE2 (x64)") => RegisterFunctionsAuto();
}
public partial class Sse3IntrinsicsModule : IntrinsicsModuleBase
{
public Sse3IntrinsicsModule() : base("SSE3") => RegisterFunctionsAuto();
}
public partial class Sse41IntrinsicsModule : IntrinsicsModuleBase
{
public Sse41IntrinsicsModule() : base("SSE4.1") => RegisterFunctionsAuto();
}
public partial class Sse41X64IntrinsicsModule : IntrinsicsModuleBase
{
public Sse41X64IntrinsicsModule() : base("SSE4.1 (x64)") => RegisterFunctionsAuto();
}
public partial class Sse42IntrinsicsModule : IntrinsicsModuleBase
{
public Sse42IntrinsicsModule() : base("SSE4.2") => RegisterFunctionsAuto();
}
public partial class Sse42X64IntrinsicsModule : IntrinsicsModuleBase
{
public Sse42X64IntrinsicsModule() : base("SSE4.2 (x64)") => RegisterFunctionsAuto();
}
public partial class Ssse3IntrinsicsModule : IntrinsicsModuleBase
{
public Ssse3IntrinsicsModule() : base("SSSE3") => RegisterFunctionsAuto();
}
public partial class AvxIntrinsicsModule : IntrinsicsModuleBase
{
public AvxIntrinsicsModule() : base("AVX") => RegisterFunctionsAuto();
}
public partial class Avx2IntrinsicsModule : IntrinsicsModuleBase
{
public Avx2IntrinsicsModule() : base("AVX2") => RegisterFunctionsAuto();
}
public partial class AesIntrinsicsModule : IntrinsicsModuleBase
{
public AesIntrinsicsModule() : base("AES") => RegisterFunctionsAuto();
}
public partial class Bmi1IntrinsicsModule : IntrinsicsModuleBase
{
public Bmi1IntrinsicsModule() : base("BMI1") => RegisterFunctionsAuto();
}
public partial class Bmi1X64IntrinsicsModule : IntrinsicsModuleBase
{
public Bmi1X64IntrinsicsModule() : base("BMI1 (x64)") => RegisterFunctionsAuto();
}
public partial class Bmi2IntrinsicsModule : IntrinsicsModuleBase
{
public Bmi2IntrinsicsModule() : base("BMI2") => RegisterFunctionsAuto();
}
public partial class Bmi2X64IntrinsicsModule : IntrinsicsModuleBase
{
public Bmi2X64IntrinsicsModule() : base("BMI2 (x64)") => RegisterFunctionsAuto();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Scriban.Syntax;
namespace Kalk.Core.Modules.HardwareIntrinsics
{
public abstract class IntrinsicsModuleBase : KalkModuleWithFunctions
{
public const string CategoryIntrinsics = "Vector Hardware Intrinsics";
protected IntrinsicsModuleBase(string name) : base(name)
{
}
protected void ProcessAction<TArg1Base, TArg1>(object arg1, Action<TArg1> func)
where TArg1Base : unmanaged where TArg1: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
func(nativeArg1);
}
protected void ProcessAction<TArg1Base, TArg1, TArg2Base, TArg2>(object arg1, object arg2, Action<TArg1, TArg2> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
func(nativeArg1, nativeArg2);
}
protected void ProcessAction<TArg1Base, TArg1, TArg2Base, TArg2>(object arg1, int arg1Align, object arg2, Action<TArg1, TArg2> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1, arg1Align);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
func(nativeArg1, nativeArg2);
}
protected void ProcessAction<TArg1Base, TArg1, TArg2Base, TArg2, TArg3Base, TArg3>(object arg1, object arg2, object arg3, Action<TArg1, TArg2, TArg3> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
where TArg3Base : unmanaged where TArg3: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
var nativeArg3 = ToArg<TArg3Base, TArg3>(2, arg3);
func(nativeArg1, nativeArg2, nativeArg3);
}
protected object ProcessFunc<TArg1Base, TArg1, TResultBase, TResult>(object arg1, Func<TArg1, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeResult = func(nativeArg1);
return ToResult<TResultBase, TResult>(nativeResult);
}
protected object ProcessFunc<TArg1Base, TArg1, TResultBase, TResult>(object arg1, int arg1Align, Func<TArg1, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1, arg1Align);
var nativeResult = func(nativeArg1);
return ToResult<TResultBase, TResult>(nativeResult);
}
protected object ProcessFunc<TArg1Base, TArg1, TArg2Base, TArg2, TResultBase, TResult>(object arg1, object arg2, Func<TArg1, TArg2, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
var nativeResult = func(nativeArg1, nativeArg2);
return ToResult<TResultBase, TResult>(nativeResult);
}
protected object ProcessFunc<TArg1Base, TArg1, TArg2Base, TArg2, TArg3Base, TArg3, TResultBase, TResult>(object arg1, object arg2, object arg3, Func<TArg1, TArg2, TArg3, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
where TArg3Base : unmanaged where TArg3: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
var nativeArg3 = ToArg<TArg3Base, TArg3>(2, arg3);
var nativeResult = func(nativeArg1, nativeArg2, nativeArg3);
return ToResult<TResultBase, TResult>(nativeResult);
}
protected object ProcessFunc<TArg1Base, TArg1, TArg2Base, TArg2, TArg3Base, TArg3, TArg4Base, TArg4, TResultBase, TResult>(object arg1, object arg2, object arg3, object arg4, Func<TArg1, TArg2, TArg3, TArg4, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
where TArg3Base : unmanaged where TArg3: unmanaged
where TArg4Base : unmanaged where TArg4: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
var nativeArg3 = ToArg<TArg3Base, TArg3>(2, arg3);
var nativeArg4 = ToArg<TArg4Base, TArg4>(3, arg4);
var nativeResult = func(nativeArg1, nativeArg2, nativeArg3, nativeArg4);
return ToResult<TResultBase, TResult>(nativeResult);
}
protected object ProcessFunc<TArg1Base, TArg1, TArg2Base, TArg2, TArg3Base, TArg3, TArg4Base, TArg4, TArg5Base, TArg5, TResultBase, TResult>(object arg1, object arg2, object arg3, object arg4, object arg5, Func<TArg1, TArg2, TArg3, TArg4, TArg5, TResult> func)
where TArg1Base : unmanaged where TArg1: unmanaged
where TArg2Base : unmanaged where TArg2: unmanaged
where TArg3Base : unmanaged where TArg3: unmanaged
where TArg4Base : unmanaged where TArg4: unmanaged
where TArg5Base : unmanaged where TArg5: unmanaged
where TResultBase : unmanaged where TResult: unmanaged
{
var nativeArg1 = ToArg<TArg1Base, TArg1>(0, arg1);
var nativeArg2 = ToArg<TArg2Base, TArg2>(1, arg2);
var nativeArg3 = ToArg<TArg3Base, TArg3>(2, arg3);
var nativeArg4 = ToArg<TArg4Base, TArg4>(3, arg4);
var nativeArg5 = ToArg<TArg5Base, TArg5>(4, arg5);
var nativeResult = func(nativeArg1, nativeArg2, nativeArg3, nativeArg4, nativeArg5);
return ToResult<TResultBase, TResult>(nativeResult);
}
private T ToArg<TBase, T>(int argIndex, object value, int align = 0) where T : unmanaged where TBase: unmanaged
{
if (typeof(T) == typeof(IntPtr))
{
var buffer = value as KalkNativeBuffer;
if (buffer == null)
{
throw new ScriptArgumentException(argIndex, "Expecting a byte buffer. Use malloc(size) to pass data to this argument.");
}
var ptr = buffer.GetPointer();
if (align != 0)
{
var remainder = (long) ptr & (long) align - 1;
if (remainder != 0)
{
throw new ScriptArgumentException(argIndex, $"Invalid memory alignment. Expecting an alignment on {align} bytes, but the bytebuffer offset is off by {remainder} bytes");
}
}
var rawPtr = Unsafe.As<IntPtr, T>(ref ptr);
return rawPtr;
}
var targetSize = Unsafe.SizeOf<T>();
var baseElementSize = Unsafe.SizeOf<TBase>();
var dimension = targetSize / baseElementSize;
if (dimension == 1)
{
// Handle Vector64
if (typeof(System.Runtime.Intrinsics.Vector64<long>) == typeof(T))
{
var tValue = Engine.ToObject<long>(argIndex, value);
return (T)(object)(System.Runtime.Intrinsics.Vector64.Create(tValue));
}
if (typeof(System.Runtime.Intrinsics.Vector64<ulong>) == typeof(T))
{
var tValue = Engine.ToObject<ulong>(argIndex, value);
return (T)(object)(System.Runtime.Intrinsics.Vector64.Create(tValue));
}
if (typeof(System.Runtime.Intrinsics.Vector64<double>) == typeof(T))
{
var tValue = Engine.ToObject<double>(argIndex, value);
return (T)(object)(System.Runtime.Intrinsics.Vector64.Create(tValue));
}
return Engine.ToObject<T>(argIndex, value);
}
else
{
Debug.Assert(dimension > 1);
Span<TBase> elements = stackalloc TBase[dimension];
if (value is KalkVector vec)
{
var span = vec.AsSpan();
var minSize = Math.Min(targetSize, span.Length);
span = span.Slice(0, minSize);
span.CopyTo(MemoryMarshal.Cast<TBase, byte>(elements));
}
else
{
var leftValueT = Engine.ToObject<TBase>(argIndex, value);
for (int i = 0; i < dimension; i++)
{
elements[i] = leftValueT;
}
}
return Unsafe.As<TBase, T>(ref elements[0]);
}
}
private object ToResult<TBase, T>(T result) where T: unmanaged where TBase: unmanaged
{
var targetSize = Unsafe.SizeOf<T>();
var baseElementSize = Unsafe.SizeOf<TBase>();
var dimension = targetSize / baseElementSize;
var span = MemoryMarshal.Cast<T, TBase>(MemoryMarshal.CreateSpan(ref result, 1));
if (dimension == 1)
{
return span[0];
}
else
{
var vector = new KalkVector<TBase>(dimension);
for (int i = 0; i < dimension; i++)
{
vector[i] = span[i];
}
return vector;
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
using System.IO;
namespace Kalk.Core.Modules
{
/// <summary>
/// Modules that contains standard units.
/// </summary>
[KalkExportModule(ModuleName)]
public partial class StandardUnitsModule : KalkModuleWithFunctions
{
private const string ModuleName = "StandardUnits";
public StandardUnitsModule() : base(ModuleName)
{
RegisterFunctionsAuto();
}
protected override void Initialize()
{
}
protected override void Import()
{
base.Import();
var countBeforeImport = Engine.Units.Count;
Engine.LoadSystemFileFromResource("units.kalk");
var deltaCount = Engine.Units.Count - countBeforeImport;
Engine.WriteHighlightLine($"# {deltaCount} units successfully imported from module `{Name}`.");
}
}
}

View File

@@ -0,0 +1,385 @@
using System;
using System.Collections;
using Scriban.Functions;
using Scriban.Runtime;
namespace Kalk.Core.Modules
{
/// <summary>
/// Modules that provides string functions (e.g `upcase`, `downcase`, `regex_escape`...).
/// </summary>
[KalkExportModule(ModuleName)]
public partial class StringModule : KalkModuleWithFunctions
{
private const string ModuleName = "Strings";
public const string CategoryString = "Text Functions";
public StringModule() : base(ModuleName)
{
RegisterFunctionsAuto();
}
/// <summary>Escapes a string with escape characters.</summary>
/// <param name="text">The input string</param>
/// <returns>The two strings concatenated</returns>
/// <example>
/// ```kalk
/// >>> "Hel\tlo\n\"W\\orld" |> escape
/// # "Hel\tlo\n\"W\\orld" |> escape
/// out = "Hel\\tlo\\n\\\"W\\\\orld"
/// ```
/// </example>
[KalkExport("escape", CategoryString)]
public string StringEscape(string text) => StringFunctions.Escape(text);
/// <summary>
/// Converts the first character of the passed string to a upper case character.
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The capitalized input string</returns>
/// <example>
/// ```kalk
/// >>> "test" |> capitalize
/// # "test" |> capitalize
/// out = "Test"
/// ```
/// </example>
[KalkExport("capitalize", CategoryString)]
public string StringCapitalize(string text) => StringFunctions.Capitalize(text);
/// <summary>
/// Converts the first character of each word in the passed string to a upper case character.
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The capitalized input string</returns>
/// <example>
/// ```kalk
/// >>> "This is easy" |> capitalize_words
/// # "This is easy" |> capitalize_words
/// out = "This Is Easy"
/// ```
/// </example>
[KalkExport("capitalize_words", CategoryString)]
public string StringCapitalizeWords(string text) => StringFunctions.Capitalizewords(text);
/// <summary>Converts the string to lower case.</summary>
/// <param name="text">The input string</param>
/// <returns>The input string lower case</returns>
/// <example>
/// ```kalk
/// >>> "TeSt" |> downcase
/// # "TeSt" |> downcase
/// out = "test"
/// ```
/// </example>
[KalkExport("downcase", CategoryString)]
public string StringDowncase(string text) => StringFunctions.Downcase(text);
/// <summary>Converts the string to uppercase</summary>
/// <param name="text">The input string</param>
/// <returns>The input string upper case</returns>
/// <example>
/// ```kalk
/// >>> "test" |> upcase
/// # "test" |> upcase
/// out = "TEST"
/// ```
/// </example>
[KalkExport("upcase", CategoryString)]
public string StringUpcase(string text) => StringFunctions.Upcase(text);
/// <summary>
/// Returns a boolean indicating whether the input string ends with the specified string `value`.
/// </summary>
/// <param name="text">The input string</param>
/// <param name="end">The string to look for</param>
/// <returns><c>true</c> if `text` ends with the specified string `value`</returns>
/// <example>
/// ```kalk
/// >>> "This is easy" |> endswith "easy"
/// # "This is easy" |> endswith("easy")
/// out = true
/// >>> "This is easy" |> endswith "none"
/// # "This is easy" |> endswith("none")
/// out = false
/// ```
/// </example>
[KalkExport("endswith", CategoryString)]
public KalkBool StringEndsWith(string text, string end) => StringFunctions.EndsWith(text, end);
/// <summary>Returns a url handle from the input string.</summary>
/// <param name="text">The input string</param>
/// <returns>A url handle</returns>
/// <example>
/// ```kalk
/// >>> '100% M @ Ms!!!' |> handleize
/// # '100% M @ Ms!!!' |> handleize
/// out = "100-m-ms"
/// ```
/// </example>
[KalkExport("handleize", CategoryString)]
public string StringHandleize(string text) => StringFunctions.Handleize(text);
/// <summary>
/// Removes any whitespace characters on the **left** side of the input string.
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The input string without any left whitespace characters</returns>
/// <example>
/// ```kalk
/// >>> ' too many spaces' |> lstrip
/// # ' too many spaces' |> lstrip
/// out = "too many spaces"
/// ```
/// </example>
[KalkExport("lstrip", CategoryString)]
public string StringLeftStrip(string text) => StringFunctions.LStrip(text);
/// <summary>
/// Outputs the singular or plural version of a string based on the value of a number.
/// </summary>
/// <param name="number">The number to check</param>
/// <param name="singular">The singular string to return if number is == 1</param>
/// <param name="plural">The plural string to return if number is != 1</param>
/// <returns>The singular or plural string based on number</returns>
/// <example>
/// ```kalk
/// >>> 3 |> pluralize('product', 'products')
/// # 3 |> pluralize('product', 'products')
/// out = "products"
/// ```
/// </example>
[KalkExport("pluralize", CategoryString)]
public string StringPluralize(int number, string singular, string plural) => StringFunctions.Pluralize(number, singular, plural);
/// <summary>
/// Removes any whitespace characters on the **right** side of the input string.
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The input string without any left whitespace characters</returns>
/// <example>
/// ```kalk
/// >>> ' too many spaces ' |> rstrip
/// # ' too many spaces ' |> rstrip
/// out = " too many spaces"
/// ```
/// </example>
[KalkExport("rstrip", CategoryString)]
public string StringRightStrip(string text) => StringFunctions.RStrip(text);
/// <summary>
/// The `split` function takes on a substring as a parameter.
/// The substring is used as a delimiter to divide a string into an array. You can output different parts of an array using `array` functions.
/// </summary>
/// <param name="text">The input string</param>
/// <param name="match">The string used to split the input `text` string</param>
/// <returns>An enumeration of the substrings</returns>
/// <example>
/// ```kalk
/// >>> "Hi, how are you today?" |> split ' '
/// # "Hi, how are you today?" |> split(' ')
/// out = ["Hi,", "how", "are", "you", "today?"]
/// ```
/// </example>
[KalkExport("split", CategoryString)]
public IEnumerable StringSplit(string text, string match) => StringFunctions.Split(text, match);
/// <summary>
/// Returns a boolean indicating whether the input string starts with the specified string `value`.
/// </summary>
/// <param name="text">The input string</param>
/// <param name="start">The string to look for</param>
/// <returns><c>true</c> if `text` starts with the specified string `value`</returns>
/// <example>
/// ```kalk
/// >>> "This is easy" |> startswith "This"
/// # "This is easy" |> startswith("This")
/// out = true
/// >>> "This is easy" |> startswith "easy"
/// # "This is easy" |> startswith("easy")
/// out = false
/// ```
/// </example>
[KalkExport("startswith", CategoryString)]
public KalkBool StringStartsWith(string text, string start) => StringFunctions.StartsWith(text, start);
/// <summary>
/// Removes any whitespace characters on the **left** and **right** side of the input string.
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The input string without any left and right whitespace characters</returns>
/// <example>
/// ```kalk
/// >>> ' too many spaces ' |> strip
/// # ' too many spaces ' |> strip
/// out = "too many spaces"
/// ```
/// </example>
[KalkExport("strip", CategoryString)]
public string StringStrip(string text) => StringFunctions.Strip(text);
/// <summary>Removes any line breaks/newlines from a string.</summary>
/// <param name="text">The input string</param>
/// <returns>The input string without any breaks/newlines characters</returns>
/// <example>
/// ```kalk
/// >>> "This is a string.\r\n With \nanother \rstring" |> strip_newlines
/// # "This is a string.\r\n With \nanother \rstring" |> strip_newlines
/// out = "This is a string. With another string"
/// ```
/// </example>
[KalkExport("strip_newlines", CategoryString)]
public string StringStripNewlines(string text) => StringFunctions.StripNewlines(text);
/// <summary>
/// Pads a string with leading spaces to a specified total length.
/// </summary>
/// <param name="text">The input string</param>
/// <param name="width">The number of characters in the resulting string</param>
/// <returns>The input string padded</returns>
/// <example>
/// ```kalk
/// >>> "world" |> pad_left 10
/// # "world" |> pad_left(10)
/// out = " world"
/// ```
/// </example>
[KalkExport("pad_left", CategoryString)]
public string StringPadLeft(string text, int width) => StringFunctions.PadLeft(text, width);
/// <summary>
/// Pads a string with trailing spaces to a specified total length.
/// </summary>
/// <param name="text">The input string</param>
/// <param name="width">The number of characters in the resulting string</param>
/// <returns>The input string padded</returns>
/// <example>
/// ```kalk
/// >>> "hello" |> pad_right 10
/// # "hello" |> pad_right(10)
/// out = "hello "
/// ```
/// </example>
[KalkExport("pad_right", CategoryString)]
public string StringPadRight(string text, int width) => StringFunctions.PadRight(text, width);
/// <summary>
/// Escapes a minimal set of characters (`\`, `*`, `+`, `?`, `|`, `{`, `[`, `(`,`)`, `^`, `$`,`.`, `#`, and white space)
/// by replacing them with their escape codes.
/// This instructs the regular expression engine to interpret these characters literally rather than as metacharacters.
/// </summary>
/// <param name="text">The input string that contains the text to convert.</param>
/// <returns>A string of characters with metacharacters converted to their escaped form.</returns>
/// <example>
/// ```kalk
/// >>> "(abc.*)" |> regex_escape
/// # "(abc.*)" |> regex_escape
/// out = "\\(abc\\.\\*\\)"
/// ```
/// </example>
[KalkExport("regex_escape", CategoryString)]
public string RegexEscape(string text) => RegexFunctions.Escape(text);
/// <summary>
/// Searches an input string for a substring that matches a regular expression pattern and returns an array with the match occurences.
/// </summary>
/// <param name="text">The string to search for a match.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="options">A string with regex options, that can contain the following option characters (default is `null`):
/// - `i`: Specifies case-insensitive matching.
/// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string.
/// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`).
/// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`.
/// </param>
/// <returns>An array that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match.</returns>
/// <example>
/// ```kalk
/// >>> "this is a text123" |> regex_match `(\w+) a ([a-z]+\d+)`
/// # "this is a text123" |> regex_match(`(\w+) a ([a-z]+\d+)`)
/// out = ["is a text123", "is", "text123"]
/// ```
/// </example>
[KalkExport("regex_match", CategoryString)]
public ScriptArray RegexMatch(string text, string pattern, string options = null) => RegexFunctions.Match(Engine, text, pattern, options);
/// <summary>
/// Searches an input string for multiple substrings that matches a regular expression pattern and returns an array with the match occurences.
/// </summary>
/// <param name="text">The string to search for a match.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="options">A string with regex options, that can contain the following option characters (default is `null`):
/// - `i`: Specifies case-insensitive matching.
/// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string.
/// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`).
/// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`.
/// </param>
/// <returns>An array of matches that contains all the match groups. The first group contains the entire match. The other elements contain regex matched groups `(..)`. An empty array returned means no match.</returns>
/// <example>
/// ```kalk
/// >>> "this is a text123" |> regex_matches `(\w+)`
/// # "this is a text123" |> regex_matches(`(\w+)`)
/// out = [["this", "this"], ["is", "is"], ["a", "a"], ["text123", "text123"]]
/// ```
/// </example>
[KalkExport("regex_matches", CategoryString)]
public ScriptArray RegexMatches(string text, string pattern, string options = null) => RegexFunctions.Matches(Engine, text, pattern, options);
/// <summary>
/// In a specified input string, replaces strings that match a regular expression pattern with a specified replacement string.
/// </summary>
/// <param name="text">The string to search for a match.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="replace">The replacement string.</param>
/// <param name="options">A string with regex options, that can contain the following option characters (default is `null`):
/// - `i`: Specifies case-insensitive matching.
/// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string.
/// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`).
/// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`.
/// </param>
/// <returns>A new string that is identical to the input string, except that the replacement string takes the place of each matched string. If pattern is not matched in the current instance, the method returns the current instance unchanged.</returns>
/// <example>
/// ```kalk
/// >>> "abbbbcccd" |> regex_replace("b+c+","-Yo-")
/// # "abbbbcccd" |> regex_replace("b+c+", "-Yo-")
/// out = "a-Yo-d"
/// ```
/// </example>
[KalkExport("regex_replace", CategoryString)]
public string RegexReplace(string text, string pattern, string replace, string options = null) => RegexFunctions.Replace(Engine, text, pattern, replace, options);
/// <summary>
/// Splits an input string into an array of substrings at the positions defined by a regular expression match.
/// </summary>
/// <param name="text">The string to split.</param>
/// <param name="pattern">The regular expression pattern to match.</param>
/// <param name="options">A string with regex options, that can contain the following option characters (default is `null`):
/// - `i`: Specifies case-insensitive matching.
/// - `m`: Multiline mode. Changes the meaning of `^` and `$` so they match at the beginning and end, respectively, of any line, and not just the beginning and end of the entire string.
/// - `s`: Specifies single-line mode. Changes the meaning of the dot `.` so it matches every character (instead of every character except `\n`).
/// - `x`: Eliminates unescaped white space from the pattern and enables comments marked with `#`.
/// </param>
/// <returns>A string array.</returns>
/// <example>
/// ```kalk
/// >>> "a, b , c, d" |> regex_split `\s*,\s*`
/// # "a, b , c, d" |> regex_split(`\s*,\s*`)
/// out = ["a", "b", "c", "d"]
/// ```
/// </example>
[KalkExport("regex_split", CategoryString)]
public ScriptArray RegexSplit(string text, string pattern, string options = null) => RegexFunctions.Split(Engine, text, pattern, options);
/// <summary>Converts any escaped characters in the input string.</summary>
/// <param name="text">The input string containing the text to convert.</param>
/// <returns>A string of characters with any escaped characters converted to their unescaped form.</returns>
/// <example>
/// ```kalk
/// >>> "\\(abc\\.\\*\\)" |> regex_unescape
/// # "\\(abc\\.\\*\\)" |> regex_unescape
/// out = "(abc.*)"
/// ```
/// </example>
[KalkExport("regex_unescape", CategoryString)]
public string RegexUnescape(string text) => RegexFunctions.Unescape(text);
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace Kalk.Core
{
public interface IKalkVectorObject
{
int Length { get; }
Type ElementType { get; }
}
public interface IKalkVectorObject<T> : IKalkVectorObject
{
object Transform(Func<T, T> apply);
}
}

View File

@@ -0,0 +1,178 @@
using System;
using Scriban;
using Scriban.Parsing;
using Scriban.Syntax;
namespace Kalk.Core
{
public struct KalkBool : IEquatable<KalkBool>, IFormattable, IConvertible, IScriptConvertibleTo, IScriptConvertibleFrom, IScriptCustomTypeInfo
{
private int _value;
public KalkBool(bool value)
{
_value = value ? -1 : 0;
}
public override string ToString()
{
return ToString(null, null);
}
public bool TryConvertFrom(TemplateContext context, SourceSpan span, object value)
{
try
{
_value = Convert.ToBoolean(value) ? -1 : 0;
return true;
}
catch
{
// ignore
}
return false;
}
public bool TryConvertTo(TemplateContext context, SourceSpan span, Type type, out object value)
{
if (type == typeof(bool))
{
value = (bool) this;
return true;
}
try
{
value = Convert.ChangeType(_value, type);
return true;
}
catch
{
// ignore
}
value = null;
return false;
}
public string ToString(string? format, IFormatProvider? formatProvider)
{
return _value != 0 ? "true" : "false";
}
public bool Equals(KalkBool other)
{
return _value == other._value;
}
public override bool Equals(object obj)
{
return obj is KalkBool other && Equals(other);
}
public override int GetHashCode()
{
return _value;
}
public static bool operator ==(KalkBool left, KalkBool right)
{
return left.Equals(right);
}
public static bool operator !=(KalkBool left, KalkBool right)
{
return !left.Equals(right);
}
public static implicit operator bool(KalkBool b) => b._value != 0;
public static implicit operator KalkBool(bool b) => new KalkBool(b);
TypeCode IConvertible.GetTypeCode()
{
return _value.GetTypeCode();
}
bool IConvertible.ToBoolean(IFormatProvider? provider)
{
return ((IConvertible) _value).ToBoolean(provider);
}
byte IConvertible.ToByte(IFormatProvider? provider)
{
return ((IConvertible) _value).ToByte(provider);
}
char IConvertible.ToChar(IFormatProvider? provider)
{
return ((IConvertible) _value).ToChar(provider);
}
DateTime IConvertible.ToDateTime(IFormatProvider? provider)
{
return ((IConvertible) _value).ToDateTime(provider);
}
decimal IConvertible.ToDecimal(IFormatProvider? provider)
{
return ((IConvertible) _value).ToDecimal(provider);
}
double IConvertible.ToDouble(IFormatProvider? provider)
{
return ((IConvertible) _value).ToDouble(provider);
}
short IConvertible.ToInt16(IFormatProvider? provider)
{
return ((IConvertible) _value).ToInt16(provider);
}
int IConvertible.ToInt32(IFormatProvider? provider)
{
return ((IConvertible) _value).ToInt32(provider);
}
long IConvertible.ToInt64(IFormatProvider? provider)
{
return ((IConvertible) _value).ToInt64(provider);
}
sbyte IConvertible.ToSByte(IFormatProvider? provider)
{
return ((IConvertible) _value).ToSByte(provider);
}
float IConvertible.ToSingle(IFormatProvider? provider)
{
return ((IConvertible) _value).ToSingle(provider);
}
string IConvertible.ToString(IFormatProvider? provider)
{
return _value.ToString(provider);
}
object IConvertible.ToType(Type conversionType, IFormatProvider? provider)
{
return ((IConvertible) _value).ToType(conversionType, provider);
}
ushort IConvertible.ToUInt16(IFormatProvider? provider)
{
return ((IConvertible) _value).ToUInt16(provider);
}
uint IConvertible.ToUInt32(IFormatProvider? provider)
{
return ((IConvertible) _value).ToUInt32(provider);
}
ulong IConvertible.ToUInt64(IFormatProvider? provider)
{
return ((IConvertible) _value).ToUInt64(provider);
}
public string TypeName => "bool";
}
}

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using System.Text;
using Consolus;
using Scriban;
using Scriban.Parsing;
using Scriban.Syntax;
namespace Kalk.Core
{
// https://github.com/google/palette.js/tree/master
[ScriptTypeName("color")]
public abstract class KalkColor : KalkVector<byte>
{
protected KalkColor(int dimension) : base(dimension)
{
}
protected KalkColor(IReadOnlyList<byte> list) : base(list)
{
}
protected KalkColor(KalkVector<byte> values) : base(values)
{
}
public int rgb => (int)((r << 16) | (g << 8) | b);
public string Name
{
get
{
KalkColorRgb.TryGetKnownColor(rgb, out var name);
return name;
}
}
public override IEnumerable<string> GetMembers()
{
for (int i = 0; i < Math.Min(4, Length); i++)
{
switch (i)
{
case 0:
yield return "r";
break;
case 1:
yield return "g";
break;
case 2:
yield return "b";
break;
case 3:
yield return "a";
break;
}
}
yield return "name";
}
public override bool TryGetValue(TemplateContext context, SourceSpan span, string member, out object result)
{
if (member == "name")
{
result = Name;
return true;
}
return base.TryGetValue(context, span, member, out result);
}
private static float Clamp01(float value) => Math.Clamp(value, 0.0f, 1.0f);
public KalkVector<float> GetFloatVector(int targetDimension)
{
return this is KalkColorRgb
? targetDimension == 4 ?
new KalkVector<float>(Clamp01(this[0] / 255.0f), Clamp01(this[1] / 255.0f), Clamp01(this[2] / 255.0f), 1.0f):
new KalkVector<float>(Clamp01(this[0] / 255.0f), Clamp01(this[1] / 255.0f), Clamp01(this[2] / 255.0f))
: new KalkVector<float>(Clamp01(this[0] / 255.0f), Clamp01(this[1] / 255.0f), Clamp01(this[2] / 255.0f), Clamp01(this[3] / 255.0f));
}
protected override object GetSwizzleValue(ComponentKind kind, byte result)
{
return kind == ComponentKind.xyzw ? (object)Clamp01(result / 255.0f) : result;
}
protected override byte TransformComponentToSet(TemplateContext context, SourceSpan span, ComponentKind kind, object value)
{
return kind == ComponentKind.xyzw ? (byte)(Clamp01(context.ToObject<float>(span, value)) * 255) : base.TransformComponentToSet(context, span, kind, value);
}
protected override KalkVector NewVector(ComponentKind kind, IReadOnlyList<byte> list)
{
if (kind == ComponentKind.xyzw)
{
if (list.Count == 4)
{
return new KalkVector<float>(Clamp01(list[0] / 255.0f), Clamp01(list[1] / 255.0f), Clamp01(list[2] / 255.0f), Clamp01(list[3] / 255.0f));
}
if (list.Count == 3)
{
return new KalkVector<float>(Clamp01(list[0] / 255.0f), Clamp01(list[1] / 255.0f), Clamp01(list[2] / 255.0f));
}
if (list.Count == 2)
{
return new KalkVector<float>(Clamp01(list[0] / 255.0f), Clamp01(list[1] / 255.0f));
}
throw new InvalidOperationException("Cannot create a float vector type from this rgba components");
}
else
{
return list.Count == 4 ? new KalkColorRgba(list[0], list[1], list[2], list[3]) : list.Count == 3 ? new KalkColorRgb(list[0], list[1], list[2]) : base.NewVector(kind, list);
}
}
protected abstract override KalkVector<byte> NewVector(int length);
public abstract override string TypeName { get; }
public override string ToString(string format, IFormatProvider formatProvider)
{
var engine = formatProvider as KalkEngine;
var builder = new StringBuilder();
builder.Append(TypeName);
builder.Append('(');
var length = this is KalkColorRgb ? 3 : 4;
for (int i = 0; i < length; i++)
{
if (i > 0) builder.Append(", ");
builder.Append(engine != null ? engine.ObjectToString(this[i]) : this[i].ToString(null, formatProvider));
}
builder.Append(')');
bool isAligned = format == "aligned";
// rgb(240, 248, 255)
// rgb(255, 255, 255, 255)
if (isAligned)
{
if (length == 3)
{
builder.Append(' ', "rgb(255, 255, 255)".Length - builder.Length);
}
else if (length == 4)
{
builder.Append(' ', "rgb(255, 255, 255, 255)".Length - builder.Length);
}
}
builder.Append(" ## ");
for (int i = 0; i < length; i++)
{
builder.Append($"{this[i]:X2}");
}
if (engine != null && engine.IsOutputSupportHighlighting)
{
builder.Append(" ");
builder.Append(ConsoleStyle.BackgroundRgb(this[0], this[1], this[2]));
builder.Append(" ");
builder.Append(ConsoleStyle.Reset);
}
// Add known color name
if (KalkColorRgb.TryGetKnownColor(rgb, out var colorName))
{
builder.Append(isAligned ? $" {colorName,-20}" : $" {colorName}");
}
builder.Append(" ##");
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections;
using Scriban;
using Scriban.Syntax;
namespace Kalk.Core
{
public abstract class KalkColorConstructor : KalkVectorConstructor<byte>
{
protected KalkColorConstructor(int dimension) : base(dimension)
{
}
protected abstract override KalkVector<byte> NewVector(int dimension);
protected override byte GetArgumentValue(TemplateContext context, object arg)
{
switch (arg)
{
case float f32:
return (byte)(255 * Math.Clamp(f32, 0.0f, 1.0f));
case double f64:
return (byte)(255 * Math.Clamp(f64, 0.0, 1.0));
case decimal dec:
return (byte)(255 * Math.Clamp(dec, 0.0m, 1.0m));
default:
return base.GetArgumentValue(context, arg);
}
}
protected override void ProcessSingleArgument(TemplateContext context, ref int index, object arg, KalkVector<byte> vector)
{
int value;
switch (arg)
{
case string rgbStr:
try
{
if (!KalkColorRgb.TryGetKnownColor(rgbStr, out value))
{
value = int.Parse(rgbStr.TrimStart('#'), System.Globalization.NumberStyles.HexNumber);
}
}
catch
{
throw new ScriptArgumentException(0, $"Expecting a known color (e.g `AliceBlue`) or an hexadecimal rgb string (e.g #FF80C2) instead of `{rgbStr}`. Type `colors` for listing known colors.");
}
break;
default:
if (arg is IList)
{
base.ProcessSingleArgument(context, ref index, arg, vector);
return;
}
value = context.ToObject<int>(context.CurrentSpan, arg);
break;
}
vector[index++] = (byte)((value >> 16) & 0xFF);
vector[index++] = (byte)((value >> 8) & 0xFF);
vector[index++] = (byte)(value & 0xFF);
if (Dimension == 4)
{
vector[index++] = (byte)((value >> 24) & 0xFF);
}
}
}
}

View File

@@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Scriban.Syntax;
namespace Kalk.Core
{
[ScriptTypeName("rgb")]
public class KalkColorRgb : KalkColor
{
public KalkColorRgb() : base(4)
{
this[3] = 0xFF;
}
public KalkColorRgb(KalkColorRgb values) : base(values)
{
this[3] = 0xFF;
}
public KalkColorRgb(byte r, byte g, byte b) : this()
{
this[0] = r;
this[1] = g;
this[2] = b;
}
public KalkColorRgb(int rgb) : this()
{
this[0] = (byte)((rgb >> 16) & 0xFF);
this[1] = (byte)((rgb >> 8) & 0xFF);
this[2] = (byte)(rgb & 0xFF);
this[3] = 0xFF;
}
public override string TypeName => "rgb";
public override KalkVector Clone()
{
return new KalkColorRgb(this);
}
protected override KalkVector<byte> NewVector(int length) => new KalkColorRgb();
public static bool TryGetKnownColor(string color, out int rgb)
{
if (color == null) throw new ArgumentNullException(nameof(color));
return KnownColors.TryGetValue(color, out rgb);
}
public static bool TryGetKnownColor(int rgb, out string color)
{
return ReverseKnownColors.TryGetValue(rgb, out color);
}
public static List<KalkColorRgb> GetKnownColors()
{
return KnownColors.OrderBy(x => x.Key).Select(x => new KalkColorRgb(x.Value)).ToList();
}
private static readonly Dictionary<string, int> KnownColors = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
{"AliceBlue", 0xF0F8FF},
{"AntiqueWhite", 0xFAEBD7},
{"Aqua", 0x00FFFF},
{"Aquamarine", 0x7FFFD4},
{"Azure", 0xF0FFFF},
{"Beige", 0xF5F5DC},
{"Bisque", 0xFFE4C4},
{"Black", 0x0},
{"BlanchedAlmond", 0xFFEBCD},
{"Blue", 0x0000FF},
{"BlueViolet", 0x8A2BE2},
{"Brown", 0xA52A2A},
{"BurlyWood", 0xDEB887},
{"CadetBlue", 0x5F9EA0},
{"Chartreuse", 0x7FFF00},
{"Chocolate", 0xD2691E},
{"Coral", 0xFF7F50},
{"CornflowerBlue", 0x6495ED},
{"Cornsilk", 0xFFF8DC},
{"Crimson", 0xDC143C},
{"Cyan", 0x00FFFF},
{"DarkBlue", 0x00008B},
{"DarkCyan", 0x008B8B},
{"DarkGoldenrod", 0xB8860B},
{"DarkGray", 0xA9A9A9},
{"DarkGreen", 0x6400},
{"DarkKhaki", 0xBDB76B},
{"DarkMagenta", 0x8B008B},
{"DarkOliveGreen", 0x556B2F},
{"DarkOrange", 0xFF8C00},
{"DarkOrchid", 0x9932CC},
{"DarkRed", 0x8B0000},
{"DarkSalmon", 0xE9967A},
{"DarkSeaGreen", 0x8FBC8F},
{"DarkSlateBlue", 0x483D8B},
{"DarkSlateGray", 0x2F4F4F},
{"DarkTurquoise", 0x00CED1},
{"DarkViolet", 0x9400D3},
{"DeepPink", 0xFF1493},
{"DeepSkyBlue", 0x00BFFF},
{"DimGray", 0x696969},
{"DodgerBlue", 0x1E90FF},
{"Firebrick", 0xB22222},
{"FloralWhite", 0xFFFAF0},
{"ForestGreen", 0x228B22},
{"Fuchsia", 0xFF00FF},
{"Gainsboro", 0xDCDCDC},
{"GhostWhite", 0xF8F8FF},
{"Gold", 0xFFD700},
{"Goldenrod", 0xDAA520},
{"Gray", 0x808080},
{"Green", 0x8000},
{"GreenYellow", 0xADFF2F},
{"Honeydew", 0xF0FFF0},
{"HotPink", 0xFF69B4},
{"IndianRed", 0xCD5C5C},
{"Indigo", 0x4B0082},
{"Ivory", 0xFFFFF0},
{"Khaki", 0xF0E68C},
{"Lavender", 0xE6E6FA},
{"LavenderBlush", 0xFFF0F5},
{"LawnGreen", 0x7CFC00},
{"LemonChiffon", 0xFFFACD},
{"LightBlue", 0xADD8E6},
{"LightCoral", 0xF08080},
{"LightCyan", 0xE0FFFF},
{"LightGoldenrodYellow", 0xFAFAD2},
{"LightGreen", 0x90EE90},
{"LightGray", 0xD3D3D3},
{"LightPink", 0xFFB6C1},
{"LightSalmon", 0xFFA07A},
{"LightSeaGreen", 0x20B2AA},
{"LightSkyBlue", 0x87CEFA},
{"LightSlateGray", 0x778899},
{"LightSteelBlue", 0xB0C4DE},
{"LightYellow", 0xFFFFE0},
{"Lime", 0x00FF00},
{"LimeGreen", 0x32CD32},
{"Linen", 0xFAF0E6},
{"Magenta", 0xFF00FF},
{"Maroon", 0x800000},
{"MediumAquamarine", 0x66CDAA},
{"MediumBlue", 0x0000CD},
{"MediumOrchid", 0xBA55D3},
{"MediumPurple", 0x9370DB},
{"MediumSeaGreen", 0x3CB371},
{"MediumSlateBlue", 0x7B68EE},
{"MediumSpringGreen", 0x00FA9A},
{"MediumTurquoise", 0x48D1CC},
{"MediumVioletRed", 0xC71585},
{"MidnightBlue", 0x191970},
{"MintCream", 0xF5FFFA},
{"MistyRose", 0xFFE4E1},
{"Moccasin", 0xFFE4B5},
{"NavajoWhite", 0xFFDEAD},
{"Navy", 0x80},
{"OldLace", 0xFDF5E6},
{"Olive", 0x808000},
{"OliveDrab", 0x6B8E23},
{"Orange", 0xFFA500},
{"OrangeRed", 0xFF4500},
{"Orchid", 0xDA70D6},
{"PaleGoldenrod", 0xEEE8AA},
{"PaleGreen", 0x98FB98},
{"PaleTurquoise", 0xAFEEEE},
{"PaleVioletRed", 0xDB7093},
{"PapayaWhip", 0xFFEFD5},
{"PeachPuff", 0xFFDAB9},
{"Peru", 0xCD853F},
{"Pink", 0xFFC0CB},
{"Plum", 0xDDA0DD},
{"PowderBlue", 0xB0E0E6},
{"Purple", 0x800080},
{"RebeccaPurple", 0x663399},
{"Red", 0xFF0000},
{"RosyBrown", 0xBC8F8F},
{"RoyalBlue", 0x41690},
{"SaddleBrown", 0x8B4513},
{"Salmon", 0xFA8072},
{"SandyBrown", 0xF4A460},
{"SeaGreen", 0x2E8B57},
{"SeaShell", 0xFFF5EE},
{"Sienna", 0xA0522D},
{"Silver", 0xC0C0C0},
{"SkyBlue", 0x87CEEB},
{"SlateBlue", 0x6A5ACD},
{"SlateGray", 0x708090},
{"Snow", 0xFFFAFA},
{"SpringGreen", 0x00FF7F},
{"SteelBlue", 0x4682B4},
{"Tan", 0xD2B48C},
{"Teal", 0x8080},
{"Thistle", 0xD8BFD8},
{"Tomato", 0xFF6347},
{"Turquoise", 0x40E0D0},
{"Violet", 0xEE82EE},
{"Wheat", 0xF5DEB3},
{"White", 0xFFFFFF},
{"WhiteSmoke", 0xF5F5F5},
{"Yellow", 0xFFFF00},
{"YellowGreen", 0x9ACD32},
};
private static readonly Dictionary<int, string> ReverseKnownColors = new Dictionary<int, string>();
static KalkColorRgb()
{
foreach (var knownColor in KnownColors)
{
if (!ReverseKnownColors.ContainsKey(knownColor.Value))
{
ReverseKnownColors.Add(knownColor.Value, knownColor.Key);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
namespace Kalk.Core
{
public class KalkColorRgbConstructor : KalkColorConstructor
{
public KalkColorRgbConstructor() : base(3)
{
}
protected override KalkVector<byte> NewVector(int dimension) => new KalkColorRgb();
}
}

View File

@@ -0,0 +1,42 @@
using Scriban.Syntax;
namespace Kalk.Core
{
[ScriptTypeName("rgba")]
public class KalkColorRgba : KalkColor
{
public KalkColorRgba() : base(4)
{
}
public KalkColorRgba(byte r, byte g, byte b, byte a) : this()
{
this[0] = r;
this[1] = g;
this[2] = b;
this[3] = a;
}
public KalkColorRgba(int rgb) : this()
{
this[0] = (byte)((rgb >> 16) & 0xFF);
this[1] = (byte)((rgb >> 8) & 0xFF);
this[2] = (byte)(rgb & 0xFF);
this[3] = (byte)((rgb >> 24) & 0xFF);
}
public KalkColorRgba(KalkColorRgba values) : base(values)
{
}
public override string TypeName => "rgba";
public override KalkVector Clone()
{
return new KalkColorRgba(this);
}
protected override KalkVector<byte> NewVector(int length) => new KalkColorRgba();
}
}

View File

@@ -0,0 +1,11 @@
namespace Kalk.Core
{
public class KalkColorRgbaConstructor : KalkColorConstructor
{
public KalkColorRgbaConstructor() : base(4)
{
}
protected override KalkVector<byte> NewVector(int dimension) => new KalkColorRgba();
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,159 @@
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using Kalk.Core.Helpers;
using Scriban;
using Scriban.Helpers;
using Scriban.Syntax;
namespace Kalk.Core
{
public class KalkVectorConstructor<T> : KalkConstructor where T : unmanaged
{
public KalkVectorConstructor(int dimension)
{
Dimension = dimension;
}
public int Dimension { get; }
public KalkVector<T> Invoke(TemplateContext context, object[] arguments)
{
if (arguments.Length == 0)
{
return NewVector(Dimension);
}
var vector = NewVector(Dimension);
int index = 0;
if (arguments.Length == 1)
{
var arg = arguments[0];
// Replace implicitly Rgb/Rgba to xyzw
if (arg is KalkColor color)
{
if (this is KalkColorConstructor)
{
var colorLength = color is KalkColorRgb ? 3 : 4;
if (Dimension != colorLength)
{
if (Dimension == 3) // 4 to 3
{
arg = new KalkVector<byte>(color.r, color.g, color.b);
}
else // 3 to 4
{
Debug.Assert(Dimension == 4);
arg = new KalkVector<byte>(color.r, color.g, color.b, 255);
}
}
}
else
{
arg = color.GetFloatVector(Dimension);
}
}
var argLength = GetArgLength(arg, true);
var length = index + argLength;
if (length != Dimension)
{
throw new ScriptArgumentException(0, $"Invalid number of arguments for {vector.TypeName}. Expecting {Dimension} arguments instead of {length}.");
}
ProcessSingleArgument(context, ref index, arg, vector);
}
else
{
for (var i = 0; i < arguments.Length; i++)
{
var arg = arguments[i];
var argLength = GetArgLength(arg, false);
var length = index + argLength;
if (length > Dimension)
{
throw new ScriptArgumentException(i, $"Invalid number of arguments for {vector.TypeName}. Expecting {Dimension} arguments instead of {length}.");
}
ProcessArgument(context, ref index, arg, vector);
}
}
if (index != Dimension)
{
throw new ScriptArgumentException(arguments.Length - 1, $"Invalid number of arguments for {vector.TypeName}. Expecting {Dimension} arguments instead of {index}.");
}
return vector;
}
private int GetArgLength(object arg, bool isSingleArg)
{
if (arg is IList list)
{
int argLength = 0;
for (int i = 0; i < list.Count; i++)
{
argLength += GetArgLength(list[i], false);
}
return argLength;
}
return isSingleArg ? Dimension : 1;
}
private void AddListItem(TemplateContext context, ref int index, object arg, KalkVector<T> vector)
{
if (arg is IList list)
{
var count = list.Count;
for (int j = 0; j < count; j++)
{
AddListItem(context, ref index, list[j], vector);
}
}
else
{
var value = GetArgumentValue(context, arg);
vector[index++] = value;
}
}
protected virtual void ProcessSingleArgument(TemplateContext context, ref int index, object arg, KalkVector<T> vector)
{
if (arg is IList list)
{
AddListItem(context, ref index, list, vector);
}
else
{
var value = GetArgumentValue(context, arg);
for (int j = 0; j < Dimension; j++)
{
vector[index++] = value;
}
}
}
protected void ProcessArgument(TemplateContext context, ref int index, object arg, KalkVector<T> vector)
{
if (arg is IList list)
{
AddListItem(context, ref index, list, vector);
}
else
{
var value = GetArgumentValue(context, arg);
vector[index++] = value;
}
}
protected virtual KalkVector<T> NewVector(int dimension) => new KalkVector<T>(dimension);
protected virtual T GetArgumentValue(TemplateContext context, object value)
{
return context.ToObject<T>(context.CurrentSpan, value);
}
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using Scriban;
using Scriban.Parsing;
using Scriban.Runtime;
using Scriban.Syntax;
namespace Kalk.Core
{
public class PrimitiveSwizzleAccessor : IObjectAccessor
{
public static readonly PrimitiveSwizzleAccessor Default = new PrimitiveSwizzleAccessor();
private PrimitiveSwizzleAccessor()
{
}
public int GetMemberCount(TemplateContext context, SourceSpan span, object target)
{
return 0;
}
public IEnumerable<string> GetMembers(TemplateContext context, SourceSpan span, object target)
{
yield break;
}
public bool HasMember(TemplateContext context, SourceSpan span, object target, string member)
{
return IsSwizzle(member);
}
public bool TryGetValue(TemplateContext context, SourceSpan span, object target, string member, out object value)
{
var targetFloat = context.ToObject<float>(span, target);
if (member.Length == 1)
{
value = targetFloat;
return true;
}
var vector = new KalkVector<float>(member.Length);
int index = 0;
for(int i = 0; i < member.Length; i++)
{
var c = member[i];
switch (c)
{
case 'x':
vector[index] = targetFloat;
break;
case 'y':
vector[index] = targetFloat;
break;
case 'z':
vector[index] = targetFloat;
break;
case 'w':
vector[index] = targetFloat;
break;
}
index++;
}
value = vector;
return true;
}
public bool TrySetValue(TemplateContext context, SourceSpan span, object target, string member, object value)
{
throw new ScriptRuntimeException(span, "Cannot set a member on a primitive");
}
public bool TryGetItem(TemplateContext context, SourceSpan span, object target, object index, out object value)
{
throw new NotImplementedException();
}
public bool TrySetItem(TemplateContext context, SourceSpan span, object target, object index, object value)
{
throw new NotImplementedException();
}
public bool HasIndexer => false;
public Type IndexType => typeof(int);
private static bool IsSwizzle(string text)
{
if (text.Length > 4) return false;
for (int i = 0; i < text.Length; i++)
{
var c = text[i];
switch (c)
{
case 'x':
case 'y':
case 'z':
case 'w':
break;
default:
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,538 @@
using System;
using Scriban.Syntax;
namespace Kalk.Core.Modules
{
public sealed partial class TypesModule : KalkModuleWithFunctions
{
public const string CategoryTypeConstructors = "Type Constructors";
public const string CategoryVectorTypeConstructors = "Type Vector Constructors";
private static readonly KalkColorRgbConstructor RgbConstructor = new KalkColorRgbConstructor();
private static readonly KalkColorRgbaConstructor RgbaConstructor = new KalkColorRgbaConstructor();
public TypesModule() : base("Types")
{
IsBuiltin = true;
RegisterFunctionsAuto();
}
/// <summary>
/// Creates an unsigned byte value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>An unsigned byte value</returns>
/// <example>
/// ```kalk
/// >>> byte
/// # byte
/// out = 0
/// >>> byte 0
/// # byte(0)
/// out = 0
/// >>> byte 255
/// # byte(255)
/// out = 255
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> byte 256
/// Unable to convert type `int` to `byte`
/// ```
/// </test>
[KalkExport("byte", CategoryTypeConstructors)]
public byte CreateByte(object value = null) => value == null ? (byte)0 : Engine.ToObject<byte>(0, value);
/// <summary>
/// Creates a signed-byte value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A signed-byte value</returns>
/// <example>
/// ```kalk
/// >>> sbyte
/// # sbyte
/// out = 0
/// >>> sbyte 0
/// # sbyte(0)
/// out = 0
/// >>> sbyte 127
/// # sbyte(127)
/// out = 127
/// >>> sbyte(-128)
/// # sbyte(-128)
/// out = -128
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> sbyte 128
/// Unable to convert type `int` to `sbyte`
/// ```
/// </test>
[KalkExport("sbyte", CategoryTypeConstructors)]
public sbyte CreateSByte(object value = null) => value == null ? (sbyte)0 : Engine.ToObject<sbyte>(0, value);
/// <summary>
/// Creates a signed-short (16-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A signed-short (16-bit) value</returns>
/// <example>
/// ```kalk
/// >>> short
/// # short
/// out = 0
/// >>> short 0
/// # short(0)
/// out = 0
/// >>> short 32767
/// # short(32767)
/// out = 32_767
/// >>> short(-32768)
/// # short(-32768)
/// out = -32_768
/// >>> short 32768
/// Unable to convert type `int` to `short`
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> short 32768
/// Unable to convert type `int` to `short`
/// ```
/// </test>
[KalkExport("short", CategoryTypeConstructors)]
public short CreateShort(object value = null) => value == null ? (short)0 : Engine.ToObject<short>(0, value);
/// <summary>
/// Creates an unsigned short (16-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>An unsigned short (16-bit) value</returns>
/// <example>
/// ```kalk
/// >>> ushort
/// # ushort
/// out = 0
/// >>> ushort 0
/// # ushort(0)
/// out = 0
/// >>> ushort 65535
/// # ushort(65535)
/// out = 65_535
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> ushort 65536
/// Unable to convert type `int` to `ushort`
/// ```
/// </test>
[KalkExport("ushort", CategoryTypeConstructors)]
public ushort CreateUShort(object value = null) => value == null ? (ushort)0 : Engine.ToObject<ushort>(0, value);
/// <summary>
/// Creates an unsigned int (32-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>An unsigned int (32-bit) value</returns>
/// <example>
/// ```kalk
/// >>> uint
/// # uint
/// out = 0
/// >>> uint 0
/// # uint(0)
/// out = 0
/// >>> uint(1&lt;&lt;32 - 1)
/// # uint(1 &lt;&lt; 32 - 1)
/// out = 4_294_967_295
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> uint 1 &lt;&lt; 32
/// Unable to convert type `long` to `uint`
/// ```
/// </test>
[KalkExport("uint", CategoryTypeConstructors)]
public uint CreateUInt(object value = null) => value == null ? 0U : Engine.ToObject<uint>(0, value);
/// <summary>
/// Creates a signed-int (32-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A signed-int (32-bit) value</returns>
/// <example>
/// ```kalk
/// >>> int
/// # int
/// out = 0
/// >>> int 0
/// # int(0)
/// out = 0
/// >>> int(1 &lt;&lt; 31 - 1)
/// # int(1 &lt;&lt; 31 - 1)
/// out = 2_147_483_647
/// >>> int(-(1&lt;&lt;31))
/// # int(-(1 &lt;&lt; 31))
/// out = -2_147_483_648
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> int 1 &lt;&lt; 31
/// Unable to convert type `long` to int
/// ```
/// </test>
[KalkExport("int", CategoryTypeConstructors)]
public int CreateInt(object value = null) => value == null ? 0 : Engine.ToObject<int>(0, value);
/// <summary>
/// Creates an unsigned long (64-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>An unsigned long (64-bit) value</returns>
/// <example>
/// ```kalk
/// >>> ulong
/// # ulong
/// out = 0
/// >>> ulong 0
/// # ulong(0)
/// out = 0
/// >>> ulong(1 &lt;&lt; 64 - 1)
/// # ulong(1 &lt;&lt; 64 - 1)
/// out = 18_446_744_073_709_551_615
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> ulong 1 &lt;&lt; 64
/// Unable to convert type `bigint` to `ulong`
/// ```
/// </test>
[KalkExport("ulong", CategoryTypeConstructors)]
public ulong CreateULong(object value = null) => value == null ? 0UL : Engine.ToObject<ulong>(0, value);
/// <summary>
/// Creates a signed-long (64-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A signed-long (64-bit) value</returns>
/// <example>
/// ```kalk
/// >>> long
/// # long
/// out = 0
/// >>> long 0
/// # long(0)
/// out = 0
/// >>> long(1 &lt;&lt; 63 - 1)
/// # long(1 &lt;&lt; 63 - 1)
/// out = 9_223_372_036_854_775_807
/// >>> long(-(1&lt;&lt;63))
/// # long(-(1 &lt;&lt; 63))
/// out = -9_223_372_036_854_775_808
/// ```
/// </example>
/// <test>
/// ```kalk
/// >>> long 1 &lt;&lt; 63
/// Unable to convert type `bigint` to `long`
/// ```
/// </test>
[KalkExport("long", CategoryTypeConstructors)]
public long CreateLong(object value = null) => value == null ? 0L : Engine.ToObject<long>(0, value);
/// <summary>
/// Creates a boolean value (32-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A boolean (32-bit) value</returns>
/// <example>
/// ```kalk
/// >>> bool 1
/// # bool(1)
/// out = true
/// >>> bool 0
/// # bool(0)
/// out = false
/// >>> bool true
/// # bool(true)
/// out = true
/// >>> bool false
/// # bool(false)
/// out = false
/// ```
/// </example>
[KalkExport("bool", CategoryTypeConstructors)]
public KalkBool CreateBool(object value = null) => value != null && Engine.ToObject<KalkBool>(0, value);
/// <summary>
/// Creates a float value (32-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A float (32-bit) value</returns>
/// <example>
/// ```kalk
/// >>> float(1)
/// # float(1)
/// out = 1
/// >>> float(-1)
/// # float(-1)
/// out = -1
/// >>> float(100000000000)
/// # float(100000000000)
/// out = 1E+11
/// ```
/// </example>
[KalkExport("float", CategoryTypeConstructors)]
public float CreateFloat(object value = null) => value == null ? 0.0f : Engine.ToObject<float>(0, value);
/// <summary>
/// Creates a half float value (16-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A half float (16-bit) value</returns>
/// <example>
/// ```kalk
/// >>> half(1)
/// # half(1)
/// out = 1
/// >>> half(-1)
/// # half(-1)
/// out = -1
/// >>> half(1000.5)
/// # half(1000.5)
/// out = 1000.5
/// >>> kind out
/// # kind(out)
/// out = "half"
/// ```
/// </example>
[KalkExport("half", CategoryTypeConstructors)]
public KalkHalf CreateHalf(object value = null) => value == null ? (KalkHalf)0.0f : Engine.ToObject<KalkHalf>(0, value);
/// <summary>
/// Creates a double value (64-bit) value.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>A double (64-bit) value</returns>
/// <example>
/// ```kalk
/// >>> double(1)
/// # double(1)
/// out = 1
/// >>> double(-1)
/// # double(-1)
/// out = -1
/// >>> double(100000000000)
/// # double(100000000000)
/// out = 100000000000
/// >>> double(1&lt;&lt;200)
/// # double(1 &lt;&lt; 200)
/// out = 1.6069380442589903E+60
/// ```
/// </example>
[KalkExport("double", CategoryTypeConstructors)]
public double CreateDouble(object value = null) => value == null ? 0.0 : Engine.ToObject<double>(0, value);
/// <summary>
/// Creates a vector of the specified element type, with the number of elements and optional values.
/// </summary>
/// <param name="name">The element type of the vector (e.g float).</param>
/// <param name="dimension">The dimension of the vector.</param>
/// <param name="arguments">The optional values (must have 1 or dimension elements).</param>
/// <returns>A matrix of the specified row x column.</returns>
/// <example>
/// ```kalk
/// >>> vector(float, 4, 5..8)
/// # vector(float, 4, 5..8)
/// out = float4(5, 6, 7, 8)
/// ```
/// </example>
[KalkExport("vector", CategoryVectorTypeConstructors)]
public object CreateVector(ScriptVariable name, int dimension, params object[] arguments)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (dimension <= 1) throw new ArgumentOutOfRangeException(nameof(dimension), "Invalid dimension. Expecting a value > 1.");
switch (name.Name)
{
case "int":
switch (dimension)
{
case 2: return CreateInt2(arguments);
case 3: return CreateInt3(arguments);
case 4: return CreateInt4(arguments);
case 8: return CreateInt8(arguments);
case 16: return CreateInt16(arguments);
}
return new KalkVectorConstructor<int>(dimension).Invoke(Engine, arguments);
case "bool":
switch (dimension)
{
case 2: return CreateBool2(arguments);
case 3: return CreateBool3(arguments);
case 4: return CreateBool4(arguments);
case 8: return CreateBool8(arguments);
case 16: return CreateBool16(arguments);
}
return new KalkVectorConstructor<KalkBool>(dimension).Invoke(Engine, arguments);
case "float":
switch (dimension)
{
case 2: return CreateFloat2(arguments);
case 3: return CreateFloat3(arguments);
case 4: return CreateFloat4(arguments);
case 8: return CreateFloat8(arguments);
case 16: return CreateFloat16(arguments);
}
return new KalkVectorConstructor<float>(dimension).Invoke(Engine, arguments);
case "half":
switch (dimension)
{
case 2: return CreateHalf2(arguments);
case 3: return CreateHalf3(arguments);
case 4: return CreateHalf4(arguments);
case 8: return CreateHalf8(arguments);
case 16: return CreateHalf16(arguments);
case 32: return CreateHalf32(arguments);
}
return new KalkVectorConstructor<KalkHalf>(dimension).Invoke(Engine, arguments);
case "double":
switch (dimension)
{
case 2: return CreateDouble2(arguments);
case 3: return CreateDouble3(arguments);
case 4: return CreateDouble4(arguments);
case 8: return CreateDouble8(arguments);
}
return new KalkVectorConstructor<double>(dimension).Invoke(Engine, arguments);
}
throw new ArgumentException($"Unsupported vector type {name.Name}. Only bool, int, float and double are supported", nameof(name));
}
/// <summary>
/// Creates an rgb vector type with the specified argument values.
/// </summary>
/// <param name="arguments">The vector item values. The total number of values must equal the dimension of the vector (3). The arguments can be:
/// - No values: All items of the rgb vector are initialized with the value 0.
/// - an integer value: `rgb(0xAABBCC)` will extract the RGB 8-bits component values (AA: R, BB: G, CC: B).
/// - a string value: `rgb("#AABBCC")` or `rgb("AABBCC")` will extract the RGB 8-bits component values (AA: R, BB: G, CC: B).
/// - an array value: `rgb([0xAA,0xBB,0xCC])` will initialize rgb elements with the array elements. The size of the array must match the size of the rgb vector (3).
/// - A combination of vectors/single values (e.g `rgb(float3(0.1, 0.2, 0.3)`).
/// </param>
/// <returns>A rgb vector initialized with the specified arguments</returns>
/// <example>
/// ```kalk
/// >>> rgb(0xAABBCC)
/// # rgb(11189196)
/// out = rgb(170, 187, 204) ## AABBCC ##
/// >>> rgb("#AABBCC")
/// # rgb("#AABBCC")
/// out = rgb(170, 187, 204) ## AABBCC ##
/// >>> rgb("AABBCC")
/// # rgb("AABBCC")
/// out = rgb(170, 187, 204) ## AABBCC ##
/// >>> rgb([0xAA,0xBB,0xCC])
/// # rgb([170,187,204])
/// out = rgb(170, 187, 204) ## AABBCC ##
/// >>> out.xyz
/// # out.xyz
/// out = float3(0.6666667, 0.73333335, 0.8)
/// >>> rgb(out)
/// # rgb(out)
/// out = rgb(170, 187, 204) ## AABBCC ##
/// ```
/// </example>
[KalkExport("rgb", CategoryVectorTypeConstructors)]
public KalkColorRgb CreateRgb(params object[] arguments) => (KalkColorRgb)RgbConstructor.Invoke(Engine, arguments);
/// <summary>
/// Creates an rgba vector type with the specified argument values.
/// </summary>
/// <param name="arguments">The vector item values. The total number of values must equal the dimension of the vector (4). The arguments can be:
/// - No values: All items of the rgba vector are initialized with the value 0.
/// - an integer value: `rgba(0xFFAABBCC)` will extract the RGB 8-bits component values (FF: A, AA: R, BB: G, CC: B).
/// - a string value: `rgba("#FFAABBCC")` or `rgba("FFAABBCC")` will extract the RGB 8-bits component values (FF: A, AA: R, BB: G, CC: B).
/// - an array value: `rgba([0xAA,0xBB,0xCC,0xFF])` will initialize rgba elements with the array elements. The size of the array must match the size of the rgb vector (3).
/// - A combination of vectors/single values (e.g `rgba(float4(0.1, 0.2, 0.3, 1.0)`).
/// </param>
/// <returns>A rgb vector initialized with the specified arguments</returns>
/// <example>
/// ```kalk
/// >>> rgba(0xFFAABBCC)
/// # rgba(-5588020)
/// out = rgba(170, 187, 204, 255) ## AABBCCFF ##
/// >>> rgba("#FFAABBCC")
/// # rgba("#FFAABBCC")
/// out = rgba(170, 187, 204, 255) ## AABBCCFF ##
/// >>> rgba("FFAABBCC")
/// # rgba("FFAABBCC")
/// out = rgba(170, 187, 204, 255) ## AABBCCFF ##
/// >>> rgba([0xAA,0xBB,0xCC,0xFF])
/// # rgba([170,187,204,255])
/// out = rgba(170, 187, 204, 255) ## AABBCCFF ##
/// >>> out.xyzw
/// # out.xyzw
/// out = float4(0.6666667, 0.73333335, 0.8, 1)
/// >>> rgba(out)
/// # rgba(out)
/// out = rgba(170, 187, 204, 255) ## AABBCCFF ##
/// ```
/// </example>
[KalkExport("rgba", CategoryVectorTypeConstructors)]
public KalkColorRgba CreateRgba(params object[] arguments) => (KalkColorRgba)RgbaConstructor.Invoke(Engine, arguments);
/// <summary>
/// Creates a matrix of the specified element type, number of rows and columns and optional values.
/// </summary>
/// <param name="name">The element type of the matrix (e.g float).</param>
/// <param name="row">The number of rows.</param>
/// <param name="column">The number of columns.</param>
/// <param name="arguments">The optional values (must have 1 or row x column elements).</param>
/// <returns>A matrix of the specified row x column.</returns>
/// <example>
/// ```kalk
/// >>> matrix(float,4,3,1..12)
/// # matrix(float, 4, 3, 1..12)
/// out = float4x3(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
/// # col 0 1 2 / row
/// float3(1 , 2 , 3 ) # 0
/// float3(4 , 5 , 6 ) # 1
/// float3(7 , 8 , 9 ) # 2
/// float3(10 , 11 , 12 ) # 3
/// ```
/// </example>
[KalkExport("matrix", CategoryMatrixConstructors)]
public object CreateMatrix(ScriptVariable name, int row, int column, params object[] arguments)
{
if (name == null) throw new ArgumentNullException(nameof(name));
if (row <= 1) throw new ArgumentOutOfRangeException(nameof(row), $"Invalid row count {row}. Expecting a value > 1.");
if (column <= 1) throw new ArgumentOutOfRangeException(nameof(column), $"Invalid column count {column}. Expecting a value > 1.");
switch (name.Name)
{
case "int":
return new KalkMatrixConstructor<int>(row, column).Invoke(Engine, arguments);
case "bool":
return new KalkMatrixConstructor<KalkBool>(row, column).Invoke(Engine, arguments);
case "float":
return new KalkMatrixConstructor<float>(row, column).Invoke(Engine, arguments);
case "half":
return new KalkMatrixConstructor<KalkHalf>(row, column).Invoke(Engine, arguments);
case "double":
return new KalkMatrixConstructor<double>(row, column).Invoke(Engine, arguments);
}
throw new ArgumentException($"Unsupported matrix type {name.Name}. Only bool, int, float, half and double are supported", nameof(name));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,131 @@
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".generated.cs" #>
<#
var types = new List<(string, string, int, int, string)>();
foreach(var typeU in new string[] { "Bool", "Int", "Float", "Double", "Half" })
{
var type = typeU.ToLowerInvariant();
for(int y = 2; y <= 4; y++) {
for(int x = 2; x <= 4; x++) {
var genericType = typeU == "Bool" ? "KalkBool" : typeU == "Half" ? "KalkHalf" : type;
types.Add((type, $"{typeU}{y}x{x}", y, x, genericType));
}
}
}
var vectypes = new List<(string, string, string, int[])>() {
("byte", "Byte", "byte", new int[] {16,32,64}),
("sbyte", "SByte", "sbyte", new int[] {16,32,64}),
("short", "Short", "short", new int[] {2,4,8,16,32}),
("ushort", "UShort", "ushort", new int[] {2,4,8,16,32}),
("int", "Int", "int", new int[] {2,3,4,8,16}),
("uint", "UInt", "uint", new int[] {2,3,4,8,16}),
("long", "Long", "long", new int[] {2,3,4,8}),
("ulong", "ULong", "ulong", new int[] {2,3,4,8}),
("KalkBool", "Bool", "bool", new int[] {2,3,4,8,16}),
("float", "Float", "float", new int[] {2,3,4,8,16}),
("double", "Double", "double", new int[] {2,3,4,8}),
("KalkHalf", "Half", "half", new int[] {2,3,4,8,16,32}),
};
#>
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Date: <#= DateTime.Now #>
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using Scriban.Helpers;
using Scriban.Syntax;
namespace Kalk.Core.Modules
{
public partial class TypesModule
{
private const string CategoryMatrixConstructors = "Type Matrix Constructors";
<# foreach(var type in vectypes) { #>
<# foreach(var dim in type.Item4) { #>
private static readonly KalkVectorConstructor<<#= type.Item1 #>> <#= type.Item2 #><#= dim #>Constructor = new KalkVectorConstructor<<#= type.Item1 #>>(<#= dim #>);
<# } #>
<# } #>
<# foreach(var type in types) { #>
private static readonly KalkMatrixConstructor<<#= type.Item5 #>> <#= type.Item2 #>Constructor = new KalkMatrixConstructor<<#= type.Item5 #>>(<#= type.Item3 #>, <#= type.Item4 #>);
<# } #>
<# foreach(var type in vectypes) { #>
<# foreach(var dim in type.Item4) {
var ctor = $"{type.Item3}{dim}";
if (type.Item3 == "bool") { #>
/// <summary>Creates a vector of <#= dim #> `<#= type.Item3 #>` items.</summary>
/// <param name="arguments">The vector item values. The total number of values must equal the dimension of the vector. The arguments can be:
/// - No values: The vector is initialized with false values.
/// - a single value: `<#= ctor #>(true)` will initialize all elements with 123.
/// - an array value: `<#= ctor #>([true, false, ...])` will initialize all elements with the array elements. The size of the array must match the size of the vector.
/// - A combination of vectors/single values (e.g `<#= type.Item3 #>4(<#= type.Item3 #>2(true,false), <#= type.Item3 #>2(false,true))` or `<#= type.Item3 #>4(<#= type.Item3 #>3(false,true,true), false)`)
/// </param>
/// <returns>A <#= ctor #> vector initialized with the specified arguments</returns>
/// <example>
/// ```kalk
/// >>> <#= ctor #>(true)
/// # <#= ctor #>(true)
/// out = <#= ctor #>(<#= string.Join(", ", Enumerable.Range(1, dim).Select(x => "true")) #>)
<# } else { #>
/// <summary>Creates a vector of <#= dim #> `<#= type.Item3 #>` items.</summary>
/// <param name="arguments">The vector item values. The total number of values must equal the dimension of the vector. The arguments can be:
/// - No values: All items of the vector are initialized with the value 0.
/// - a single value: `<#= ctor #>(123)` will initialize all elements with 123.
/// - an array value: `<#= ctor #>(1..<#= dim #>)` will initialize all elements with the array elements. The size of the array must match the size of the vector.
/// - A combination of vectors/single values (e.g `<#= type.Item3 #>4(<#= type.Item3 #>2(1,2), <#= type.Item3 #>2(3,4))` or `<#= type.Item3 #>4(<#= type.Item3 #>3(1,2,3), 4)`.
/// </param>
/// <returns>A <#= ctor #> vector initialized with the specified arguments</returns>
/// <example>
/// ```kalk
/// >>> <#= ctor #>
/// # <#= ctor #>
/// out = <#= ctor #>(<#= string.Join(", ", Enumerable.Range(1, dim).Select(x => "0")) #>)
/// >>> <#= ctor #>(1..<#= dim #>)
/// # <#= ctor #>(1..<#= dim #>)
/// out = <#= ctor #>(<#= string.Join(", ", Enumerable.Range(1, dim)) #>)
<# if (dim <= 16) { #>
/// >>> <#= ctor #>(<#= string.Join(", ", Enumerable.Range(10, dim)) #>)
/// # <#= ctor #>(<#= string.Join(", ", Enumerable.Range(10, dim)) #>)
/// out = <#= ctor #>(<#= string.Join(", ", Enumerable.Range(10, dim)) #>)
<# }
}
#>
/// ```
/// </example>
[KalkExport("<#= ctor #>", CategoryVectorTypeConstructors)]
public KalkVector<<#= type.Item1 #>> Create<#= type.Item2 #><#= dim #>(params object[] arguments) => <#= type.Item2 #><#= dim #>Constructor.Invoke(Engine, arguments);
<# }
} #>
<# foreach(var type in types) {
var dim = type.Item3 * type.Item4;
var ctor_prefix = type.Item1.ToLowerInvariant();
var ctor = type.Item2.ToLowerInvariant();
#>
/// <summary>Creates a <#= type.Item3 #> (rows) x <#= type.Item4 #> (columns) matrix of <#= type.Item1 #>.</summary>
/// <param name="arguments">The matrix item values. The total number of values must equal the total dimension of the matrix. The arguments can be:
/// - No values: All items of the vector are initialized with the value 0.
/// - a single value: `<#= ctor #>(123)` will initialize all elements with 123.
/// - an array value: `<#= ctor #>(1..<#= dim #>)` will initialize all elements with the array elements. The size of the array must match the size of the vector.
/// - A combination of vectors/single values (e.g `<#= ctor_prefix #>3x4(<#= ctor_prefix #>4(1), <#= ctor_prefix #>4(2), <#= ctor_prefix #>4(3))`.
/// </param>
/// <returns>A <#= ctor #> matrix initialized with the specified arguments</returns>
[KalkExport("<#= ctor #>", CategoryMatrixConstructors)]
public KalkMatrix<<#= type.Item5 #>> Create<#= type.Item2 #>(params object[] arguments) => <#= type.Item2 #>Constructor.Invoke(Engine, arguments);
<# } #>
}
}

View File

@@ -0,0 +1,228 @@
using System;
using Scriban.Syntax;
namespace Kalk.Core.Modules
{
public partial class VectorModule
{
/// <summary>
/// Transposes the specified matrix.
/// </summary>
/// <param name="m">The matrix to transpose.</param>
/// <returns>The transposed matrix.</returns>
/// <example>
/// ```kalk
/// >>> transpose float3x4(1..12)
/// # transpose(float3x4(1..12))
/// out = float4x3(1, 5, 9, 2, 6, 10, 3, 7, 11, 4, 8, 12)
/// # col 0 1 2 / row
/// float3(1 , 5 , 9 ) # 0
/// float3(2 , 6 , 10 ) # 1
/// float3(3 , 7 , 11 ) # 2
/// float3(4 , 8 , 12 ) # 3
/// >>> transpose(out)
/// # transpose(out)
/// out = float3x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
/// # col 0 1 2 3 / row
/// float4(1 , 2 , 3 , 4 ) # 0
/// float4(5 , 6 , 7 , 8 ) # 1
/// float4(9 , 10 , 11 , 12 ) # 2
/// ```
/// </example>
[KalkExport("transpose", CategoryMathVectorMatrixFunctions)]
public static KalkMatrix Transpose(KalkMatrix m) => KalkMatrix.Transpose(m);
/// <summary>
/// Creates an identity of a squared matrix.
/// </summary>
/// <param name="m">The type of the squared matrix.</param>
/// <returns>The identity matrix of the squared matrix type.</returns>
/// <example>
/// ```kalk
/// >>> identity(float4x4)
/// # identity(float4x4)
/// out = float4x4(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)
/// # col 0 1 2 3 / row
/// float4(1 , 0 , 0 , 0 ) # 0
/// float4(0 , 1 , 0 , 0 ) # 1
/// float4(0 , 0 , 1 , 0 ) # 2
/// float4(0 , 0 , 0 , 1 ) # 3
/// ```
/// </example>
[KalkExport("identity", CategoryMathVectorMatrixFunctions)]
public static KalkMatrix Identity(KalkMatrix m) => KalkMatrix.Identity(m);
/// <summary>
/// Calculates the determinant of the specified matrix.
/// </summary>
/// <param name="m">The matrix to calculate the determinant for.</param>
/// <returns>A scalar representing the determinant of the matrix.</returns>
/// <example>
/// ```kalk
/// >>> float4x4(4,3,2,2,0,1,-3,3,0,-1,3,3,0,3,1,1)
/// # float4x4(4, 3, 2, 2, 0, 1, -3, 3, 0, -1, 3, 3, 0, 3, 1, 1)
/// out = float4x4(4, 3, 2, 2, 0, 1, -3, 3, 0, -1, 3, 3, 0, 3, 1, 1)
/// # col 0 1 2 3 / row
/// float4(4 , 3 , 2 , 2 ) # 0
/// float4(0 , 1 , -3 , 3 ) # 1
/// float4(0 , -1 , 3 , 3 ) # 2
/// float4(0 , 3 , 1 , 1 ) # 3
/// >>> determinant out
/// # determinant(out)
/// out = -240
/// ```
/// </example>
[KalkExport("determinant", CategoryMathVectorMatrixFunctions)]
public static object Determinant(KalkMatrix m) => KalkMatrix.Determinant(m);
/// <summary>
/// Calculates the inverse of the specified matrix.
/// </summary>
/// <param name="m">The matrix to calculate the inverse for.</param>
/// <returns>The inverse matrix of the specified matrix.</returns>
/// <example>
/// ```kalk
/// >>> inverse(float3x3(10,20,10,4,5,6,2,3,5))
/// # inverse(float3x3(10, 20, 10, 4, 5, 6, 2, 3, 5))
/// out = float3x3(-0.1, 1, -1, 0.11428571, -0.42857143, 0.28571427, -0.028571427, -0.14285715, 0.42857143)
/// # col 0 1 2 / row
/// float3(-0.1 , 1 , -1 ) # 0
/// float3( 0.11428571 , -0.42857143, 0.28571427) # 1
/// float3(-0.028571427, -0.14285715, 0.42857143) # 2
/// ```
/// </example>
[KalkExport("inverse", CategoryMathVectorMatrixFunctions)]
public static KalkMatrix Inverse(KalkMatrix m) => KalkMatrix.Inverse(m);
/// <summary>
/// Returns the diagonal vector of a squared matrix or a diagonal matrix from the specified vector.
/// </summary>
/// <param name="x">A vector or matrix to return the associated diagonal for.</param>
/// <returns>A diagonal vector of a matrix or a diagonal matrix of a vector.</returns>
/// <example>
/// ```kalk
/// >>> diag(float4x4(1..16))
/// # diag(float4x4(1..16))
/// out = float4(1, 6, 11, 16)
/// >>> diag(float4(1,2,3,4))
/// # diag(float4(1, 2, 3, 4))
/// out = float4x4(1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 4)
/// # col 0 1 2 3 / row
/// float4(1 , 0 , 0 , 0 ) # 0
/// float4(0 , 2 , 0 , 0 ) # 1
/// float4(0 , 0 , 3 , 0 ) # 2
/// float4(0 , 0 , 0 , 4 ) # 3
///
/// ```
/// </example>
[KalkExport("diag", CategoryMathVectorMatrixFunctions)]
public object Diagonal(object x)
{
if (x == null) throw new ArgumentNullException(nameof(x));
if (x is KalkMatrix m)
{
return KalkMatrix.Diagonal(m);
}
if (x is KalkVector v)
{
return KalkVector.Diagonal(v);
}
throw new ArgumentException($"Invalid argument type {Engine.GetTypeName(x)}. Expecting a matrix or a vector type.", nameof(x));
}
/// <summary>
/// Extract a row from the specified matrix.
/// </summary>
/// <param name="x">The matrix to extract a row from.</param>
/// <param name="index">The index of the row (zero based).</param>
/// <returns>A vector extracted from the matrix.</returns>
/// <example>
/// ```kalk
/// >>> row(float4x4(1..16), 2)
/// # row(float4x4(1..16), 2)
/// out = float4(9, 10, 11, 12)
/// ```
/// </example>
[KalkExport("row", CategoryMathVectorMatrixFunctions)]
public KalkVector GetRow(KalkMatrix x, int index)
{
return x.GetRow(index);
}
/// <summary>
/// Extract a column from the specified matrix.
/// </summary>
/// <param name="x">The matrix to extract a column from.</param>
/// <param name="index">The index of the column (zero based).</param>
/// <returns>A vector extracted from the matrix.</returns>
/// <example>
/// ```kalk
/// >>> col(float4x4(1..16), 2)
/// # col(float4x4(1..16), 2)
/// out = float4(3, 7, 11, 15)
/// ```
/// </example>
[KalkExport("col", CategoryMathVectorMatrixFunctions)]
public KalkVector GetColumn(KalkMatrix x, int index)
{
return x.GetColumn(index);
}
/// <summary>
/// Multiplies a vector x vector (dot product), or a vector x matrix, or a matrix x vector or a matrix x matrix.
/// </summary>
/// <param name="x">A left vector or a matrix.</param>
/// <param name="y">A right vector or matrix.</param>
/// <returns>The result of the multiplication.</returns>
/// <example>
/// ```kalk
/// >>> mul(float4(1,2,3,4), float4(5,6,7,8))
/// # mul(float4(1, 2, 3, 4), float4(5, 6, 7, 8))
/// out = 70
/// >>> mul(float3(3,7,5), float3x3(2,3,-4,11,8,7,2,5,3))
/// # mul(float3(3, 7, 5), float3x3(2, 3, -4, 11, 8, 7, 2, 5, 3))
/// out = float3(7, 124, 56)
/// >>> mul(float3x3(2,3,-4,11,8,7,2,5,3), float3(3,7,5))
/// # mul(float3x3(2, 3, -4, 11, 8, 7, 2, 5, 3), float3(3, 7, 5))
/// out = float3(93, 90, 52)
/// >>> mul(float3x3(2,7,4,3,2,1,9,-1,2), float3x3(1,4,6,-1,-2,5,8,7,6))
/// # mul(float3x3(2, 7, 4, 3, 2, 1, 9, -1, 2), float3x3(1, 4, 6, -1, -2, 5, 8, 7, 6))
/// out = float3x3(68, 9, 20, 37, -16, 4, 91, 64, 51)
/// # col 0 1 2 / row
/// float3(68 , 9 , 20 ) # 0
/// float3(37 , -16 , 4 ) # 1
/// float3(91 , 64 , 51 ) # 2
/// ```
/// </example>
[KalkExport("mul", CategoryMathVectorMatrixFunctions)]
public object Multiply(object x, object y)
{
if (x == null) throw new ArgumentNullException(nameof(x));
if (y == null) throw new ArgumentNullException(nameof(y));
if (x is KalkVector vx && y is KalkVector vy)
{
return KalkVector.Dot(vx, vy);
}
if (x is KalkVector vx1 && y is KalkMatrix my)
{
return KalkMatrix.Multiply(vx1, my);
}
if (x is KalkMatrix mx && y is KalkVector vy1)
{
return KalkMatrix.Multiply(mx, vy1);
}
if (x is KalkMatrix mx1 && y is KalkMatrix my2)
{
return KalkMatrix.Multiply(mx1, my2);
}
throw new ArgumentException($"Unsupported type for matrix multiplication. The combination of {Engine.GetTypeName(x)} * {Engine.GetTypeName(y)} is not supported.", nameof(x));
}
}
}

View File

@@ -0,0 +1,140 @@
using System;
using Scriban.Syntax;
namespace Kalk.Core.Modules
{
public sealed partial class VectorModule : KalkModuleWithFunctions
{
public const string CategoryMathVectorMatrixFunctions = "Math Vector/Matrix Functions";
private static readonly KalkColorRgbConstructor RgbConstructor = new KalkColorRgbConstructor();
private static readonly KalkColorRgbaConstructor RgbaConstructor = new KalkColorRgbaConstructor();
private MathModule _mathModule;
public VectorModule() : base("Vectors")
{
IsBuiltin = true;
RegisterFunctionsAuto();
}
protected override void Initialize()
{
_mathModule = Engine.GetOrCreateModule<MathModule>();
//Engine.LoadSystemFile(Path.Combine("Modules", "Vectors", "colorspaces.kalk"));
}
/// <summary>
/// Returns the length of the specified floating-point vector.
/// </summary>
/// <param name="x">The specified floating-point vector.</param>
/// <returns>A floating-point scalar that represents the length of the x parameter.</returns>
/// <example>
/// ```kalk
/// >>> length float2(1, 2)
/// # length(float2(1, 2))
/// out = 2.23606797749979
/// >>> length(-5)
/// # length(-5)
/// out = 5
/// ```
/// </example>
[KalkExport("length", CategoryMathVectorMatrixFunctions)]
public object Length(object x)
{
if (x == null) throw new ArgumentNullException(nameof(x));
if (_mathModule == null) throw new InvalidOperationException($"The module {Name} is not initialized.");
if (x is KalkVector v)
{
return _mathModule.Sqrt(new KalkDoubleValue(KalkVector.Dot(v, v)));
}
return _mathModule.Abs(new KalkCompositeValue(x));
}
/// <summary>
/// Normalizes the specified floating-point vector according to x / length(x).
/// </summary>
/// <param name="x">he specified floating-point vector.</param>
/// <returns>The normalized x parameter. If the length of the x parameter is 0, the result is indefinite.</returns>
/// <example>
/// ```kalk
/// >>> normalize float2(1,2)
/// # normalize(float2(1, 2))
/// out = float2(0.4472136, 0.8944272)
/// ```
/// </example>
[KalkExport("normalize", CategoryMathVectorMatrixFunctions)]
public object Normalize(object x)
{
return ScriptBinaryExpression.Evaluate(Engine, Engine.CurrentSpan, ScriptBinaryOperator.Divide, x, Length(x));
}
/// <summary>
/// Returns the dot product of two vectors.
/// </summary>
/// <param name="x">The first vector.</param>
/// <param name="y">The second vector.</param>
/// <returns>The dot product of the x parameter and the y parameter.</returns>
/// <example>
/// ```kalk
/// >>> dot(float3(1,2,3), float3(4,5,6))
/// # dot(float3(1, 2, 3), float3(4, 5, 6))
/// out = 32
/// >>> dot(float3(1,2,3), 4)
/// # dot(float3(1, 2, 3), 4)
/// out = 24
/// >>> dot(4, float3(1,2,3))
/// # dot(4, float3(1, 2, 3))
/// out = 24
/// >>> dot(5,6)
/// # dot(5, 6)
/// out = 30
/// ```
/// </example>
[KalkExport("dot", CategoryMathVectorMatrixFunctions)]
public object Dot(object x, object y)
{
if (x is KalkVector vx)
{
if (y is KalkVector vy)
{
return KalkVector.Dot(vx, vy);
}
return KalkVector.Dot(vx, vx.FromValue(Engine.ToObject(1, y, vx.ElementType)));
}
else if (y is KalkVector vy)
{
return KalkVector.Dot(vy.FromValue(Engine.ToObject(1, x, vy.ElementType)), vy);
}
return ScriptBinaryExpression.Evaluate(Engine, Engine.CurrentSpan, ScriptBinaryOperator.Multiply, x, y);
}
/// <summary>
/// Returns the cross product of two floating-point, 3D vectors.
/// </summary>
/// <param name="x">The first floating-point, 3D vector.</param>
/// <param name="y">The second floating-point, 3D vector.</param>
/// <returns>The cross product of the x parameter and the y parameter.</returns>
/// <example>
/// ```kalk
/// >>> cross(float3(1,2,3), float3(4,5,6))
/// # cross(float3(1, 2, 3), float3(4, 5, 6))
/// out = float3(-3, 6, -3)
/// >>> cross(float3(1,0,0), float3(0,1,0))
/// # cross(float3(1, 0, 0), float3(0, 1, 0))
/// out = float3(0, 0, 1)
/// >>> cross(float3(0,0,1), float3(0,1,0))
/// # cross(float3(0, 0, 1), float3(0, 1, 0))
/// out = float3(-1, 0, 0)
/// ```
/// </example>
[KalkExport("cross", CategoryMathVectorMatrixFunctions)]
public object Cross(KalkVector x, KalkVector y)
{
if (x == null) throw new ArgumentNullException(nameof(x));
if (y == null) throw new ArgumentNullException(nameof(y));
if (x.Length != 3 || (x.ElementType != typeof(float) && x.ElementType != typeof(double))) throw new ArgumentOutOfRangeException(nameof(x), "Expecting a float3 or double3 vector.");
if (y.Length != 3 || (y.ElementType != typeof(float) && y.ElementType != typeof(double))) throw new ArgumentOutOfRangeException(nameof(y), "Expecting a float3 or double3 vector.");
return KalkVector.Cross(x, y);
}
}
}

View File

@@ -0,0 +1,153 @@
# From http://www.chilliant.com/rgb2hsv.html
# Based on work by Sam Hocevar and Emil Persson
func HUEtoRGB(x)
$r = abs(x * 6 - 3) - 1;
$g = 2 - abs(x * 6 - 2);
$b = 2 - abs(x * 6 - 4);
ret saturate(float3($r, $g, $b));
end
func RGBtoHCV(x)
x = x.xyz;
Epsilon = 1e-10f
if x.g < x.b; $p =float4(x.bg, -1.0f, 2.0f/3.0f); else; $p = float4(x.gb, 0.0f, -1.0f/3.0f); end;
if x.r < $p.x; $q = float4($p.xyw, x.r); else; $q = float4(x.r, $p.yzx); end;
$c = $q.x - min($q.w, $q.y);
$h = abs(($q.w - $q.y) / (6.0f * $c + Epsilon) + $q.z);
ret float3($h, $c, $q.x);
end
func HSVtoRGB(x)
x = x.xyz;
$rgb = HUEtoRGB(x.x);
ret (($rgb - 1) * x.y + 1) * x.z;
end
func HSLtoRGB(x)
x = x.xyz;
$rgb = HUEtoRGB(x.x);
$c = (1 - abs(2 * x.z - 1)) * x.y;
ret ($rgb - 0.5) * $c + x.z;
end
func HCYtoRGB(x)
x = x.xyz;
# The weights of RGB contributions to luminance.
# Should sum to unity.
$HCYwts = float3(0.299f, 0.587f, 0.114f);
$rgb = HUEtoRGB(x.x);
$z = dot($rgb, $HCYwts);
if x.z < $z
x.y = x.y * x.z / $z;
else if $z < 1
x.y = x.y * (1 - x.z) / (1 - $z);
end
ret ($rgb - $z) * x.y + x.z;
end
func HCLtoRGB(x)
x = x.xyz;
HCLgamma = 3;
HCLy0 = 100;
HCLmaxL = 0.530454533953517; # == exp(HCLgamma / HCLy0) - 0.5
$rgb = float3;
if x.z != 0
H = x.x;
C = x.y;
L = x.z * HCLmaxL;
Q = exp((1 - C / (2 * L)) * (HCLgamma / HCLy0));
U = (2 * L - C) / (2 * Q - 1);
V = C / Q;
A = (H + min(frac(2 * H) / 4, frac(-2 * H) / 8)) * pi * 2;
H = H * 6;
if (H <= 0.999)
T = tan(A);
$rgb.r = 1;
$rgb.g = T / (1 + T);
else if (H <= 1.001)
$rgb.r = 1;
$rgb.g = 1;
else if (H <= 2)
T = tan(A);
$rgb.r = (1 + T) / T;
$rgb.g = 1;
else if (H <= 3)
T = tan(A);
$rgb.g = 1;
$rgb.b = 1 + T;
else if (H <= 3.999)
T = tan(A);
$rgb.g = 1 / (1 + T);
$rgb.b = 1;
else if (H <= 4.001)
$rgb.g = 0;
$rgb.b = 1;
else if (H <= 5)
T = tan(A);
$rgb.r = -1 / T;
$rgb.b = 1;
else
T = tan(A);
$rgb.r = 1;
$rgb.b = -T;
end
$rgb = $rgb * V + U;
end
ret $rgb;
end
func RGBtoHSV(x)
x = x.xyz;
Epsilon = 1e-10f
$Hcv = RGBtoHCV(x);
$s = $Hcv.y / ($Hcv.z + Epsilon);
ret float3($Hcv.x, $s, $Hcv.z);
end
func RGBtoHSL(x)
x = x.xyz;
Epsilon = 1e-10f
$hcv = RGBtoHCV(x);
$l = $hcv.z - $hcv.y * 0.5;
$s = $hcv.y / (1 - abs($l * 2 - 1) + Epsilon);
ret float3($hcv.x, $s, $l);
end
func RGBtoHCY(x)
x = x.xyz;
# Corrected by David Schaeffer
$HCYwts = float3(0.299f, 0.587f, 0.114f);
Epsilon = 1e-10f
$hcv = RGBtoHCV(x);
$y = dot(x, $HCYwts);
$z = dot(HUEtoRGB($hcv.x), $HCYwts);
if ($y < $z)
$hcv.y = $hcv.y * $z / (Epsilon + $y);
else
$hcv.y = $hcv.y * (1 - $z) / (Epsilon + 1 - $y);
end
ret float3($hcv.x, $hcv.y, $y);
end
func RGBtoHCL(x)
x = x.xyz;
HCLgamma = 3;
HCLy0 = 100;
HCLmaxL = 0.530454533953517; # == exp(HCLgamma / HCLy0) - 0.5
$hcl = float3
H = 0;
U = min(x.r, min(x.g, x.b));
V = max(x.r, max(x.g, x.b));
Q = HCLgamma / HCLy0;
$hcl.y = V - U;
if ($hcl.y != 0)
H = atan2(x.g - x.b, x.r - x.g) / pi;
Q = Q * U / V;
end
Q = exp(Q);
$hcl.x = frac(H / 2 - min(frac(H), frac(-H)) / 6);
$hcl.y = $hcl.y * Q;
$hcl.z = lerp(-U, V, Q) / (HCLmaxL * 2);
ret $hcl;
end

View File

@@ -0,0 +1,440 @@
using System;
using System.Collections;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Scriban.Functions;
using Scriban.Helpers;
using Scriban.Runtime;
namespace Kalk.Core.Modules
{
/// <summary>
/// Module that provides Web functions (e.g `url_encode`, `json`, `wget`...)
/// </summary>
[KalkExportModule(ModuleName)]
public sealed partial class WebModule : KalkModuleWithFunctions
{
private const string ModuleName = "Web";
public const string CategoryWeb = "Web & Html Functions";
public WebModule() : base(ModuleName)
{
RegisterFunctionsAuto();
}
/// <summary>
/// Converts a specified URL text into a URL-encoded.
///
/// URL encoding converts characters that are not allowed in a URL into character-entity equivalents.
/// For example, when the characters &lt; and &gt; are embedded in a block of text to be transmitted in a URL, they are encoded as %3c and %3e.
/// </summary>
/// <param name="url">The url text to encode as an URL.</param>
/// <returns>An encoded URL.</returns>
/// <example>
/// ```kalk
/// >>> url_encode "this&lt;is&gt;an:url and another part"
/// # url_encode("this&lt;is&gt;an:url and another part")
/// out = "this%3Cis%3Ean%3Aurl+and+another+part"
/// ```
/// </example>
[KalkExport("url_encode", CategoryWeb)]
public string UrlEncode(string url) => WebUtility.UrlEncode(url);
/// <summary>
/// Converts a URL-encoded string into a decoded string.
/// </summary>
/// <param name="url">The URL to decode.</param>
/// <returns>The decoded URL</returns>
/// <example>
/// ```kalk
/// >>> url_decode "this%3Cis%3Ean%3Aurl+and+another+part"
/// # url_decode("this%3Cis%3Ean%3Aurl+and+another+part")
/// out = "this&lt;is&gt;an:url and another part"
/// ```
/// </example>
[KalkExport("url_decode", CategoryWeb)]
public string UrlDecode(string url) => WebUtility.UrlDecode(url);
/// <summary>
/// Identifies all characters in a string that are not allowed in URLS, and replaces the characters with their escaped variants.
/// </summary>
/// <param name="url">The input string.</param>
/// <returns>The input string url escaped</returns>
/// <example>
/// ```kalk
/// >>> "&lt;hello&gt; &amp; &lt;scriban&gt;" |> url_escape
/// # "&lt;hello&gt; &amp; &lt;scriban&gt;" |> url_escape
/// out = "%3Chello%3E%20&amp;%20%3Cscriban%3E"
/// ```
/// </example>
[KalkExport("url_escape", CategoryWeb)]
public string UrlEscape(string url) => HtmlFunctions.UrlEscape(url);
/// <summary>
/// Encodes a HTML input string (replacing `&amp;` by `&amp;amp;`)
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The input string with HTML entities.</returns>
/// <example>
/// ```kalk
/// >>> "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_encode
/// # "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_encode
/// out = "&amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;"
/// >>> out |> html_decode
/// # out |> html_decode
/// out = "&lt;p&gt;This is a paragraph&lt;/p&gt;"
/// ```
/// </example>
[KalkExport("html_encode", CategoryWeb)]
public string HtmlEncode(string text) => HtmlFunctions.Escape(text);
/// <summary>
/// Decodes a HTML input string (replacing `&amp;amp;` by `&amp;`)
/// </summary>
/// <param name="text">The input string</param>
/// <returns>The input string removed with any HTML entities.</returns>
/// <example>
/// ```kalk
/// >>> "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_encode
/// # "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_encode
/// out = "&amp;lt;p&amp;gt;This is a paragraph&amp;lt;/p&amp;gt;"
/// >>> out |> html_decode
/// # out |> html_decode
/// out = "&lt;p&gt;This is a paragraph&lt;/p&gt;"
/// ```
/// </example>
[KalkExport("html_decode", CategoryWeb)]
public string HtmlDecode(string text)
{
return string.IsNullOrEmpty(text) ? text : System.Net.WebUtility.HtmlDecode(text);
}
/// <summary>
/// Converts to or from a JSON object depending on the value argument.
/// </summary>
/// <param name="value">A value argument:
/// - If the value is a string, it is expecting this string to be a JSON string and will convert it to the appropriate object.
/// - If the value is an array or object, it will convert it to a JSON string representation.
/// </param>
/// <returns>A JSON string or an object/array depending on the argument.</returns>
/// <example>
/// ```kalk
/// >>> json {a: 1, b: 2, c: [4,5], d: "Hello World"}
/// # json({a: 1, b: 2, c: [4,5], d: "Hello World"})
/// out = "{\"a\": 1, \"b\": 2, \"c\": [4, 5], \"d\": \"Hello World\"}"
/// >>> json out
/// # json(out)
/// out = {a: 1, b: 2, c: [4, 5], d: "Hello World"}
/// ```
/// </example>
[KalkExport("json", CategoryWeb)]
public object Json(object value)
{
switch (value)
{
case string text:
try
{
var jsonDoc = JsonDocument.Parse(text);
return ConvertFromJson(jsonDoc.RootElement);
}
catch (Exception ex)
{
throw new ArgumentException($"Unable to parse input text. Reason: {ex.Message}", nameof(value));
}
default:
{
var previousLimit = Engine.LimitToString;
try
{
Engine.LimitToString = 0;
var builder = new StringBuilder();
ConvertToJson(value, builder);
return builder.ToString();
}
catch (Exception ex)
{
throw new ArgumentException($"Unable to convert script object input to json text. Reason: {ex.Message}", nameof(value));
}
finally
{
Engine.LimitToString = previousLimit;
}
}
}
}
/// <summary>Removes any HTML tags from the input string</summary>
/// <param name="text">The input string</param>
/// <returns>The input string removed with any HTML tags</returns>
/// <example>
/// ```kalk
/// >>> "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_strip
/// # "&lt;p&gt;This is a paragraph&lt;/p&gt;" |> html_strip
/// out = "This is a paragraph"
/// ```
/// </example>
[KalkExport("html_strip", CategoryWeb)]
public string HtmlStrip(string text) => HtmlFunctions.Strip(Engine, text);
/// <summary>
/// Retrieves the content of the following URL by issuing a HTTP GET request.
/// </summary>
/// <param name="url">The URL to retrieve the content for.</param>
/// <returns>An object with the result of the request. This object contains the following members:
/// - `version`: the protocol of the version.
/// - `code`: the HTTP return code.
/// - `reason`: the HTTP reason phrase.
/// - `headers`: the HTTP returned headers.
/// - `content`: the HTTP content. Either a string if the mime type is `text/*` or an object if the mime type is `application/json` otherwise it will return a bytebuffer.
/// </returns>
/// <remarks>
/// ```
/// >>> wget "https://markdig.azurewebsites.net/"
/// # wget("https://markdig.azurewebsites.net/")
/// out = {version: "1.1", code: 200, reason: "OK", headers: {"Content-Type": "text/plain; charset=utf-8", "Content-Length": 0}, content: ""}
/// ```
/// </remarks>
[KalkExport("wget", CategoryWeb)]
public ScriptObject WebGet(string url)
{
if (url == null) throw new ArgumentNullException(nameof(url));
using (var httpClient = new HttpClient())
{
HttpResponseMessage result;
try
{
var task = Task.Run(async () => await httpClient.GetAsync(url));
task.Wait();
result = task.Result;
if (!result.IsSuccessStatusCode)
{
throw new ArgumentException($"HTTP/{result.Version} {(int)result.StatusCode} {result.ReasonPhrase}", nameof(url));
}
else
{
var headers = new ScriptObject();
foreach (var headerItem in result.Content.Headers)
{
var items = new ScriptArray(headerItem.Value);
object itemValue = items;
if (items.Count == 1)
{
var str = (string) items[0];
itemValue = str;
if (str == "true")
{
itemValue = (KalkBool)true;
}
else if (str == "false")
{
itemValue = (KalkBool)false;
}
else if (long.TryParse(str, out var longValue))
{
if (longValue >= int.MinValue && longValue <= int.MaxValue)
{
itemValue = (int) longValue;
}
else
{
itemValue = longValue;
}
}
else if (DateTime.TryParse(str, out var time))
{
itemValue = time;
}
}
headers[headerItem.Key] = itemValue;
}
var mediaType = result.Content.Headers.ContentType.MediaType;
var resultObj = new ScriptObject()
{
{"version", result.Version.ToString()},
{"code", (int) result.StatusCode},
{"reason", result.ReasonPhrase},
{"headers", headers}
};
if (mediaType.StartsWith("text/") || mediaType == "application/json")
{
var readTask = Task.Run(async () => await result.Content.ReadAsStringAsync());
readTask.Wait();
var text = readTask.Result;
if (mediaType == "application/json" && TryParseJson(text, out var jsonObject))
{
resultObj["content"] = jsonObject;
}
else
{
resultObj["content"] = readTask.Result;
}
}
else
{
var readTask = Task.Run(async () => await result.Content.ReadAsByteArrayAsync());
readTask.Wait();
resultObj["content"] = new KalkNativeBuffer(readTask.Result);
}
return resultObj;
}
}
catch (Exception ex)
{
throw new ArgumentException(ex.Message, nameof(url));
}
}
}
private static bool TryParseJson(string text, out ScriptObject scriptObj)
{
scriptObj = null;
try
{
// Make sure that we can parse the input JSON
var jsonDoc = JsonDocument.Parse(text);
var result = ConvertFromJson(jsonDoc.RootElement);
scriptObj = result as ScriptObject;
return scriptObj != null;
}
catch
{
// ignore
return false;
}
}
private void ConvertToJson(object element, StringBuilder builder)
{
if (element == null)
{
builder.Append("null");
}
else if (element is ScriptObject scriptObject)
{
builder.Append("{");
bool isFirst = true;
foreach (var item in scriptObject)
{
if (!isFirst)
{
builder.Append(", ");
}
builder.Append('\"');
builder.Append(StringFunctions.Escape(item.Key));
builder.Append('\"');
builder.Append(": ");
ConvertToJson(item.Value, builder);
isFirst = false;
}
builder.Append("}");
}
else if (element is string text)
{
builder.Append('\"');
builder.Append(StringFunctions.Escape(text));
builder.Append('\"');
}
else if (element.GetType().IsNumber())
{
builder.Append(Engine.ObjectToString(element));
}
else if (element is bool rb)
{
builder.Append(rb ? "true" : "false");
}
else if (element is KalkBool b)
{
builder.Append(b ? "true" : "false");
}
else if (element is IEnumerable it)
{
builder.Append('[');
bool isFirst = true;
foreach (var item in it)
{
if (!isFirst)
{
builder.Append(", ");
}
ConvertToJson(item, builder);
isFirst = false;
}
builder.Append(']');
}
else
{
builder.Append('\"');
builder.Append(StringFunctions.Escape(Engine.ObjectToString(element)));
builder.Append('\"');
}
}
private static object ConvertFromJson(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
var obj = new ScriptObject();
foreach (var prop in element.EnumerateObject())
{
obj[prop.Name] = ConvertFromJson(prop.Value);
}
return obj;
case JsonValueKind.Array:
var array = new ScriptArray();
foreach (var nestedElement in element.EnumerateArray())
{
array.Add(ConvertFromJson(nestedElement));
}
return array;
case JsonValueKind.String:
return element.GetString();
case JsonValueKind.Number:
if (element.TryGetInt32(out var intValue))
{
return intValue;
}
else if (element.TryGetInt64(out var longValue))
{
return longValue;
}
else if (element.TryGetUInt32(out var uintValue))
{
return uintValue;
}
else if (element.TryGetUInt64(out var ulongValue))
{
return ulongValue;
}
else if (element.TryGetDecimal(out var decimalValue))
{
return decimalValue;
}
else if (element.TryGetDouble(out var doubleValue))
{
return doubleValue;
}
else
{
throw new InvalidOperationException($"Unable to convert number {element}");
}
case JsonValueKind.True:
return (KalkBool)true;
case JsonValueKind.False:
return (KalkBool)false;
default:
return null;
}
}
}
}

73
Kalk/Kalk.Core/core.kalk Normal file
View File

@@ -0,0 +1,73 @@
# Default navigation shortcuts
shortcut(cursor_left, "left, ctrl+b", action("cursor_left"))
shortcut(cursor_right, "right, ctrl+f", action("cursor_right"))
shortcut(history_previous, "up, ctrl+p", action("history_previous"))
shortcut(history_next, "down, ctrl+n", action("history_next"))
shortcut(exit, "ctrl+z", action("exit"))
shortcut(copy, "ctrl+c", action("copy"))
shortcut(cut, "ctrl+x", action("cut"))
shortcut(paste, "ctrl+v", action("paste"))
shortcut(cursor_word_left, "ctrl+left, alt+b", action("cursor_word_left"))
shortcut(cursor_word_right, "ctrl+right, alt+f", action("cursor_word_right"))
shortcut(cursor_line_start, "home, ctrl+a", action("cursor_line_start"))
shortcut(cursor_line_end, "end, ctrl+e", action("cursor_line_end"))
shortcut(completion, "tab", action("completion"))
shortcut(delete_left, "backspace", action("delete_left"))
shortcut(delete_right, "delete, ctrl+d", action("delete_right"))
shortcut(delete_word_left, "ctrl+backspace, ctrl+alt+b", action("delete_word_left"))
shortcut(delete_word_right, "ctrl+delete, ctrl+alt+f", action("delete_word_right"))
shortcut(validate_line, "enter", action("validate_line"))
shortcut(force_validate_line, "ctrl+enter", action("force_validate_line"))
shortcut(clear_screen, "ctrl+l", cls)
# Shortcuts
shortcut(alpha, "CTRL+G A", "Α", "CTRL+G a", "α")
shortcut(beta, "CTRL+G B", "Β", "CTRL+G b", "β")
shortcut(chi, "CTRL+G C", "Χ", "CTRL+G c", "χ")
shortcut(delta, "CTRL+G D", "Δ", "CTRL+G d", "δ")
shortcut(epsilon, "CTRL+G E", "Ε", "CTRL+G e", "ε")
shortcut(eta, "CTRL+G H", "Η", "CTRL+G h", "η")
shortcut(gamma, "CTRL+G G", "Γ", "CTRL+G g", "γ")
shortcut(iota, "CTRL+G I", "Ι", "CTRL+G i", "ι")
shortcut(kappa, "CTRL+G K", "Κ", "CTRL+G k", "κ")
shortcut(lambda, "CTRL+G L", "Λ", "CTRL+G l", "λ")
shortcut(mu, "CTRL+G M", "Μ", "CTRL+G m", "μ")
shortcut(nu, "CTRL+G N", "Ν", "CTRL+G n", "ν")
shortcut(omega, "CTRL+G W", "Ω", "CTRL+G w", "ω")
shortcut(omicron, "CTRL+G O", "Ο", "CTRL+G o", "ο")
shortcut(phi, "CTRL+G F", "Φ", "CTRL+G f", "φ", "CTRL+G j", "ϕ")
shortcut(pi, "CTRL+G P", "Π", "CTRL+G p", "π")
shortcut(psi, "CTRL+G Y", "Ψ", "CTRL+G y", "ψ")
shortcut(rho, "CTRL+G R", "Ρ", "CTRL+G r", "ρ")
shortcut(sigma, "CTRL+G S", "Σ", "CTRL+G s", "σ")
shortcut(tau, "CTRL+G T", "Τ", "CTRL+G t", "τ")
shortcut(theta, "CTRL+G Q", "Θ", "CTRL+G q", "θ", "CTRL+G J", "ϑ")
shortcut(upsilon, "CTRL+G U", "Υ", "CTRL+G u", "υ")
shortcut(xi, "CTRL+G X", "Ξ", "CTRL+G x", "ξ")
shortcut(zeta, "CTRL+G Z", "Ζ", "CTRL+G z", "ζ")
# 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, Ζ, ζ)

63
Kalk/Kalk.Core/units.kalk Normal file
View File

@@ -0,0 +1,63 @@
# SI units
unit(second, 'SI Time', s, prefix: 'm,µ,n,p,f,a,z,y')
unit(meter, 'SI Length', m, prefix: 'decimal')
unit(gram, 'SI Mass', g, prefix: 'decimal')
unit(ampere, 'SI Electric current', A, prefix: 'decimal')
unit(kelvin, 'SI Thermodynamic temperature', K, prefix: 'decimal')
unit(mole, 'SI Amount of substance', mol, prefix: 'decimal')
# For candela, we can't use the prefix `cd` as it will conflict with cd = change directory
# So we keep candela only
unit(candela, 'SI Luminous intensity', prefix: 'decimal')
# SI derived https://en.wikipedia.org/wiki/SI_derived_unit
unit(hertz, 'frequency', Hz, 1/s, prefix: 'decimal')
unit(radian, 'angle', rad, plural: "radian")
unit(newton, 'solid angle', N, kg * m/s^2, prefix: 'decimal')
unit(pascal, 'pressure, stress', Pa, N/m^2, prefix: 'decimal')
unit(joule, 'energy, work, heat', J, m * N, prefix: 'decimal')
unit(watt, 'power, radiant flux', W, J/s, prefix: 'decimal')
unit(coulomb, 'electric charge or quantity of electricity', C, s * A, prefix: 'decimal')
unit(volt, 'voltage, electrical potential difference, electromotive force', V, W/A, prefix: 'decimal')
unit(farad, 'electrical capacitance', F, C/V, prefix: 'decimal')
unit(ohm, 'electrical resistance, impedance, reactance', Ω, V/A, prefix: 'decimal')
unit(siemens, 'electrical conductance', S, A/V, prefix: 'decimal')
unit(weber, 'magnetic flux', Wb, J/A, prefix: 'decimal')
unit(tesla, 'magnetic induction, magnetic flux density', T, V*s/m^2, prefix: 'decimal')
unit(henry, 'electrical inductance', H, V*s/A, prefix: 'decimal')
# unit(degree_celsius, 'electrical inductance', '℃', V*s/A, prefix: 'decimal')
unit(lumen, 'luminous flux', lm, candela, prefix: 'decimal')
unit(lux, 'illuminance', lx, lm/m^2, prefix: 'decimal')
unit(becquerel, 'radioactivity (decays per unit time)', Bq, J/kg, prefix: 'decimal')
unit(gray, 'absorbed dose (of ionizing radiation)', Gy, J/kg, prefix: 'decimal')
unit(sievert, 'equivalent dose (of ionizing radiation)', Sv, J/kg, prefix: 'decimal')
unit(katal, 'catalytic activity', kat, mol/s, prefix: 'decimal')
# Non-SI units accepted for use with the SI Units
# https://en.wikipedia.org/wiki/Non-SI_units_mentioned_in_the_SI
unit(minute, 'time in minutes', minute, 60 * s)
unit(hour, 'time in hours', h, 60 * minute)
unit(day, 'time in days', day, 24 * h)
unit(astronomical_unit, 'length', au, 149597870700 * m)
unit(degree, 'plane angle', deg, pi/180 * rad, plural: "degree")
unit(hectare, 'area', ha, 10000 * m^2)
unit(litre, 'volume', L, 1 * dm^3, prefix: 'decimal')
unit(tonne, 'mass', t, 1000 * kg)
unit(dalton, 'mass', Da, 1.66053906660e-27 * kg)
unit(electronvolt, 'energy', eV, 1.602176634e-19 * J, prefix: 'decimal')
# Binary units
unit(b, 'A digital information, data size', bits, prefix: 'Y,Z,E,P,T,G,M,k,binary')
unit(B, 'An octet, composed of 8 bits', bytes, 8 * b, prefix: 'Y,Z,E,P,T,G,M,k,binary')
# https://en.wikipedia.org/wiki/United_States_customary_units
# US - Units of length
unit(pica, 'US - Unit of length - pica', pica, 127 * mm / 30)
unit(inch, 'US - Unit of length - inch', in, 6 * pica)
unit(foot, 'US - Unit of length - foot', ft, 12 * inch)
unit(yard, 'US - Unit of length - yard', yd, 3 * ft)
unit(mile, 'US - Unit of length - mile', mi, 1760 * yd)
# US - Units of area
unit(acre, 'US - volume', acre, 43560 * foot^2)
unit(tablespoon , 'US - volume', Tbsp, 14.8 * mL)
unit(teaspoon, 'US - volume', tsp, Tbsp/3)