Skip to content

tags: - extensions - custom-functions - Callable - integration


Extension Functions

Guide to creating custom native functions for EverSharp.

Overview

EverSharp allows you to extend its functionality by creating custom native functions in C#. These functions can be called from EverSharp scripts just like built-in functions.

Callable Interface

All native functions must implement the Callable interface:

namespace Elements.Libs.EverSharp;

public interface Callable
{
    object? Call(Interpreter interpreter, List<object?> arguments);
    int? Arity();
}

Methods

Call(Interpreter interpreter, List<object?> arguments)

  • Executes the function logic
  • interpreter: The EverSharp interpreter instance
  • arguments: List of arguments passed from EverSharp
  • Returns: The result value (or null)

Arity()

  • Returns the number of expected arguments
  • Return null for variable argument functions
  • Return specific number (e.g., 2) for fixed argument count

Creating a Simple Function

Example: Square Function

Create a function that squares a number:

using Elements.Libs.EverSharp;
using Elements.Libs.EverSharp.Visitors;

namespace MyExtensions;

public class Square : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var number = (decimal)arguments[0];
        return number * number;
    }

    public int? Arity() => 1;  // Expects exactly 1 argument
}

Usage in EverSharp:

result = $Square(5); // 25

Example: Add Three Numbers

Function with multiple parameters:

public class AddThree : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var a = (decimal)arguments[0];
        var b = (decimal)arguments[1];
        var c = (decimal)arguments[2];

        return a + b + c;
    }

    public int? Arity() => 3;
}

Usage:

sum = $AddThree(10, 20, 30); // 60

Variable Argument Functions

For functions that accept any number of arguments, return null from Arity():

Example: Sum Variable Arguments

public class SumAll : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        decimal sum = 0;

        foreach (var arg in arguments)
        {
            sum += (decimal)arg;
        }

        return sum;
    }

    public int? Arity() => null;  // Variable arguments
}

Usage:

result1 = $SumAll(1, 2, 3); // 6
result2 = $SumAll(10, 20, 30, 40); // 100
result3 = $SumAll(5); // 5

Registering Extension Functions

Register custom functions when creating the EverSharpRunner:

var extensions = new Dictionary<string, Callable>
{
    ["$Square"] = new Square(),
    ["$AddThree"] = new AddThree(),
    ["$SumAll"] = new SumAll()
};

var runner = new EverSharpRunner(null, extensions);

// Now use in scripts
runner.Run(@"
    x = $Square(5);           // 25
    y = $AddThree(1, 2, 3);   // 6
    z = $SumAll(10, 20, 30);  // 60
");

Working with Different Types

String Functions

public class Capitalize : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var text = (string)arguments[0];

        if (string.IsNullOrEmpty(text))
            return text;

        return char.ToUpper(text[0]) + text.Substring(1).ToLower();
    }

    public int? Arity() => 1;
}

Usage:

name = $Capitalize("john"); // "John"

Boolean Functions

public class IsEven : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var number = (decimal)arguments[0];
        return number % 2 == 0;
    }

    public int? Arity() => 1;
}

Usage:

result = $IsEven(10); // true
result = $IsEven(7); // false

Date Functions

public class DaysUntil : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var targetDate = (DateTime)arguments[0];
        var today = DateTime.Today;

        var days = (targetDate - today).Days;
        return (decimal)days;
    }

    public int? Arity() => 1;
}

Usage:

futureDate = $ToDate("2025-12-31");
daysRemaining = $DaysUntil(futureDate);

Working with Arrays

Example: Array Average

using Elements.Libs.EverSharp.Expressions;

public class Average : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        if (arguments[0] is not InstanceArray array)
        {
            throw new Exception("Argument must be an array");
        }

        if (array.Length == 0)
        {
            return 0m;
        }

        decimal sum = 0;
        foreach (var item in array)
        {
            sum += (decimal)item;
        }

        return sum / array.Length;
    }

    public int? Arity() => 1;
}

Usage:

numbers = [10, 20, 30, 40, 50];
avg = $Average(numbers); // 30

Working with Objects

Example: Get Property Count

using Elements.Libs.EverSharp.Expressions;

public class PropertyCount : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        if (arguments[0] is not Instance obj)
        {
            throw new Exception("Argument must be an object");
        }

        return (decimal)obj.Properties.Count;
    }

    public int? Arity() => 1;
}

Usage:

person = {
  name: "John",
  age: 30,
  email: "john@example.com",
};

count = $PropertyCount(person); // 3

Error Handling

Validate Arguments

public class SafeDivide : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var numerator = (decimal)arguments[0];
        var denominator = (decimal)arguments[1];

        if (denominator == 0)
        {
            throw new Exception("Division by zero");
        }

        return numerator / denominator;
    }

    public int? Arity() => 2;
}

Type Checking

public class TypeSafeFunction : Callable
{
    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        // Check argument type
        if (arguments[0] is not decimal number)
        {
            throw new Exception($"Expected number, got {arguments[0]?.GetType().Name ?? "null"}");
        }

        return number * 2;
    }

    public int? Arity() => 1;
}

Advanced Examples

Example 1: Database Query Function

using System.Data.SqlClient;

public class QueryDatabase : Callable
{
    private readonly string connectionString;

    public QueryDatabase(string connectionString)
    {
        this.connectionString = connectionString;
    }

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var query = (string)arguments[0];
        var results = new List<object>();

        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            using (var command = new SqlCommand(query, connection))
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var row = new Instance();
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        var name = reader.GetName(i);
                        var value = reader.GetValue(i);
                        row.Set(new Token(TokenType.IDENTIFIER, name), value);
                    }
                    results.Add(row);
                }
            }
        }

        return new InstanceArray(results);
    }

    public int? Arity() => 1;
}

Registration:

var extensions = new Dictionary<string, Callable>
{
    ["$Query"] = new QueryDatabase("Server=localhost;Database=MyDb;...")
};

Usage:

customers = $Query("SELECT Id, Name, Email FROM Customers WHERE Active = 1");

activeCount = customers.Length;
firstCustomer = customers[0];

Example 2: HTTP Request Function

using System.Net.Http;
using System.Text.Json;

public class HttpGet : Callable
{
    private static readonly HttpClient client = new HttpClient();

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var url = (string)arguments[0];

        var response = client.GetStringAsync(url).Result;

        // Parse JSON response to EverSharp object
        var jsonDoc = JsonDocument.Parse(response);
        return ConvertJsonToInstance(jsonDoc.RootElement);
    }

    private object? ConvertJsonToInstance(JsonElement element)
    {
        switch (element.ValueKind)
        {
            case JsonValueKind.Object:
                var obj = new Instance();
                foreach (var prop in element.EnumerateObject())
                {
                    obj.Set(new Token(TokenType.IDENTIFIER, prop.Name),
                           ConvertJsonToInstance(prop.Value));
                }
                return obj;

            case JsonValueKind.Array:
                var arr = new List<object?>();
                foreach (var item in element.EnumerateArray())
                {
                    arr.Add(ConvertJsonToInstance(item));
                }
                return new InstanceArray(arr);

            case JsonValueKind.String:
                return element.GetString();

            case JsonValueKind.Number:
                return element.GetDecimal();

            case JsonValueKind.True:
                return true;

            case JsonValueKind.False:
                return false;

            case JsonValueKind.Null:
                return null;

            default:
                return null;
        }
    }

    public int? Arity() => 1;
}

Usage:

data = $HttpGet("https://api.example.com/data");
value = data.results[0].value;

Example 3: Logging Function

using Microsoft.Extensions.Logging;

public class LogMessage : Callable
{
    private readonly ILogger logger;

    public LogMessage(ILogger logger)
    {
        this.logger = logger;
    }

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var level = (string)arguments[0];
        var message = (string)arguments[1];

        switch (level.ToLower())
        {
            case "debug":
                logger.LogDebug(message);
                break;
            case "info":
                logger.LogInformation(message);
                break;
            case "warning":
                logger.LogWarning(message);
                break;
            case "error":
                logger.LogError(message);
                break;
            default:
                logger.LogInformation(message);
                break;
        }

        return null;
    }

    public int? Arity() => 2;
}

Usage:

$LogMessage("info", "Starting calculation");
result = calculate();
$LogMessage("debug", "Result: " + $ToString(result));

Example 4: Configuration Reader

using Microsoft.Extensions.Configuration;

public class GetConfig : Callable
{
    private readonly IConfiguration configuration;

    public GetConfig(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var key = (string)arguments[0];
        var value = configuration[key];

        if (value == null)
            return null;

        // Try to parse as number
        if (decimal.TryParse(value, out var number))
            return number;

        // Try to parse as boolean
        if (bool.TryParse(value, out var boolean))
            return boolean;

        // Return as string
        return value;
    }

    public int? Arity() => 1;
}

Usage:

maxRetries = $GetConfig("App:MaxRetries"); // 3
timeout = $GetConfig("App:Timeout"); // 30
apiUrl = $GetConfig("App:ApiUrl"); // "https://api.example.com"

Example 5: Cache Function

using System.Runtime.Caching;

public class CacheValue : Callable
{
    private static readonly MemoryCache cache = MemoryCache.Default;

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var key = (string)arguments[0];
        var value = arguments[1];
        var expirationMinutes = arguments.Count > 2 ? (int)(decimal)arguments[2] : 60;

        var policy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(expirationMinutes)
        };

        cache.Set(key, value, policy);

        return value;
    }

    public int? Arity() => null;  // 2 or 3 arguments
}

public class GetCachedValue : Callable
{
    private static readonly MemoryCache cache = MemoryCache.Default;

    public object? Call(Interpreter interpreter, List<object?> arguments)
    {
        var key = (string)arguments[0];
        return cache.Get(key);
    }

    public int? Arity() => 1;
}

Usage:

// Cache value for 30 minutes
$CacheValue("calculation_result", expensiveResult, 30);

// Retrieve cached value
cached = $GetCachedValue("calculation_result");
if (cached != null) {
  result = cached;
} else {
  result = performCalculation();
  $CacheValue("calculation_result", result, 30);
}

Function Library Pattern

Organize related functions in a class:

public static class MathExtensions
{
    public static Dictionary<string, Callable> GetFunctions()
    {
        return new Dictionary<string, Callable>
        {
            ["$Pow"] = new Power(),
            ["$Sqrt"] = new SquareRoot(),
            ["$Ceil"] = new Ceiling(),
            ["$Floor"] = new Floor()
        };
    }

    private class Power : Callable
    {
        public object? Call(Interpreter interpreter, List<object?> arguments)
        {
            var baseNum = (decimal)arguments[0];
            var exponent = (decimal)arguments[1];
            return (decimal)Math.Pow((double)baseNum, (double)exponent);
        }

        public int? Arity() => 2;
    }

    private class SquareRoot : Callable
    {
        public object? Call(Interpreter interpreter, List<object?> arguments)
        {
            var number = (decimal)arguments[0];
            return (decimal)Math.Sqrt((double)number);
        }

        public int? Arity() => 1;
    }

    // ... more functions
}

Usage:

var extensions = MathExtensions.GetFunctions();
var runner = new EverSharpRunner(null, extensions);

runner.Run(@"
    result1 = $Pow(2, 8);      // 256
    result2 = $Sqrt(144);      // 12
");

Best Practices

1. Naming Conventions

// Good: Prefix with $ for functions
["$Calculate"] = new Calculate()

// Avoid: No prefix
["Calculate"] = new Calculate()  // Might conflict with user variables

2. Null Handling

public object? Call(Interpreter interpreter, List<object?> arguments)
{
    if (arguments[0] == null)
    {
        return null;  // Or throw exception, depending on requirements
    }

    // Process non-null argument
    var value = (decimal)arguments[0];
    return value * 2;
}

3. Type Safety

public object? Call(Interpreter interpreter, List<object?> arguments)
{
    if (arguments[0] is not decimal number)
    {
        throw new ArgumentException($"Expected decimal, got {arguments[0]?.GetType().Name}");
    }

    return number * 2;
}

4. Documentation

/// <summary>
/// Calculates the compound interest for a given principal, rate, and time period.
/// </summary>
/// <remarks>
/// Usage: $CompoundInterest(principal, annualRate, years)
/// Example: $CompoundInterest(1000, 0.05, 10) returns 1628.89
/// </remarks>
public class CompoundInterest : Callable
{
    // Implementation
}

Testing Extension Functions

[Fact]
public void Square_ShouldReturnCorrectValue()
{
    var square = new Square();
    var interpreter = new Interpreter();

    var result = square.Call(interpreter, new List<object?> { 5m });

    Assert.Equal(25m, result);
}

[Fact]
public void Square_WithEverSharpRunner()
{
    var extensions = new Dictionary<string, Callable>
    {
        ["$Square"] = new Square()
    };

    var runner = new EverSharpRunner(null, extensions);

    var result = runner.RunExpression("$Square(5)");

    Assert.Equal(25m, result);
}

See Also