Systems/VNScript/BuiltinFunctions.cs
using System;
using System.Linq;
using System.Collections.Generic;
namespace VNScript;
internal static class BuiltinFunctions
{
/// <summary>
/// Contains mappings from symbols to builtin executable functions
/// </summary>
public static Dictionary<string, Value.FunctionValue> Builtins { get; } = new()
{
["="] = new Value.FunctionValue( EqualityFunction ),
["!="] = new Value.FunctionValue( NotEqualFunction ),
[">"] = new Value.FunctionValue( GreaterThanFunction ),
["<"] = new Value.FunctionValue( LessThanFunction ),
[">="] = new Value.FunctionValue( GreaterThanOrEqualFunction ),
["<="] = new Value.FunctionValue( LessThanOrEqualFunction ),
["+"] = new Value.FunctionValue( SumFunction ),
["-"] = new Value.FunctionValue( SubtractFunction ),
["*"] = new Value.FunctionValue( MulFunction ),
["set"] = new Value.FunctionValue( SetFunction ),
["defun"] = new Value.FunctionValue( DefineFunction ),
["pow"] = new Value.FunctionValue( PowFunction ),
["sqrt"] = new Value.FunctionValue( SqrtFunction ),
["if"] = new Value.FunctionValue( IfFunction ),
["not"] = new Value.FunctionValue( NotFunction ),
["and"] = new Value.FunctionValue( AndFunction ),
["or"] = new Value.FunctionValue( OrFunction ),
["when"] = new Value.FunctionValue( WhenFunction ),
["body"] = new Value.FunctionValue( ExpressionBodyFunction ),
};
private static Value.BooleanValue GreaterThanFunction( IEnvironment env, Value[] values )
{
var ( a, b ) = GetTwoNumbers( env, values, ">" );
return new Value.BooleanValue( a > b );
}
private static Value.BooleanValue LessThanFunction( IEnvironment env, Value[] values )
{
var ( a, b ) = GetTwoNumbers( env, values, "<" );
return new Value.BooleanValue( a < b );
}
private static Value.BooleanValue GreaterThanOrEqualFunction( IEnvironment environment, Value[] values )
{
var ( a, b ) = GetTwoNumbers( environment, values, ">=" );
return new Value.BooleanValue( a >= b );
}
private static Value.BooleanValue LessThanOrEqualFunction( IEnvironment environment, Value[] values )
{
var ( a, b ) = GetTwoNumbers( environment, values, "<=" );
return new Value.BooleanValue( a <= b );
}
private static Value.BooleanValue EqualityFunction( IEnvironment environment, Value[] values )
{
if ( values.Length != 2 )
{
throw new InvalidParametersException( values );
}
var v1 = values[0].Evaluate( environment );
var v2 = values[1].Evaluate( environment );
if ( v1.GetType() != v2.GetType() )
{
return new Value.BooleanValue( false );
}
return v1 switch
{
Value.BooleanValue bool1 => new Value.BooleanValue( bool1.Boolean.Equals( ((Value.BooleanValue)v2).Boolean ) ),
Value.NumberValue num1 => new Value.BooleanValue( num1.Number.Equals( ((Value.NumberValue)v2).Number ) ),
Value.StringValue str1 => new Value.BooleanValue( str1.Text.Equals( ((Value.StringValue)v2).Text ) ),
_ => new Value.BooleanValue( false )
};
}
private static Value.BooleanValue NotEqualFunction( IEnvironment environment, Value[] values )
{
var eq = EqualityFunction( environment, values );
return new Value.BooleanValue( !eq.Boolean );
}
private static Value ExpressionBodyFunction( IEnvironment environment, Value[] values )
{
if ( values.Length == 0 )
{
return Value.NoneValue.None;
}
Value lastValue = Value.NoneValue.None;
foreach ( var expression in values )
{
lastValue = expression.Evaluate( environment );
}
return lastValue;
}
private static Value IfFunction( IEnvironment environment, Value[] values )
{
if ( values.Length is < 2 or > 3 )
{
throw ParamError.Wrong( "if", "(if condition then [else])", values );
}
var condition = values[0].Evaluate( environment );
if ( condition.IsTruthy() )
{
return values[1].Evaluate( environment );
}
return values.Length == 3 ? values[2].Evaluate( environment ) : Value.NoneValue.None;
}
private static Value.BooleanValue NotFunction( IEnvironment environment, Value[] values )
{
if ( values.Length != 1 )
{
throw ParamError.Wrong( "not", "one value", values );
}
return new Value.BooleanValue( !values[0].Evaluate( environment ).IsTruthy() );
}
private static Value.BooleanValue AndFunction( IEnvironment environment, Value[] values )
{
foreach ( var expr in values )
{
var result = expr.Evaluate( environment );
if ( !result.IsTruthy() )
{
return new Value.BooleanValue( false );
}
}
return new Value.BooleanValue( true );
}
private static Value.BooleanValue OrFunction( IEnvironment environment, Value[] values )
{
foreach ( var expr in values )
{
var result = expr.Evaluate( environment );
if ( result.IsTruthy() )
{
return new Value.BooleanValue( true );
}
}
return new Value.BooleanValue( false );
}
private static Value WhenFunction( IEnvironment environment, Value[] values )
{
if ( values.Length < 2 )
{
throw new InvalidParametersException( values );
}
var condition = values[0].Evaluate( environment );
if ( !condition.IsTruthy() )
{
return Value.NoneValue.None;
}
Value last = Value.NoneValue.None;
for ( int i = 1; i < values.Length; i++ )
{
last = values[i].Evaluate( environment );
}
return last;
}
private static Value.NumberValue MulFunction( IEnvironment environment, Value[] values )
{
if ( values.Length == 0 )
{
return new Value.NumberValue( 1 ); // Identity for multiplication
}
var evaluatedValues = values.Select( v => v.Evaluate( environment ) ).ToArray();
if ( !evaluatedValues.All( v => v is Value.NumberValue ) )
{
throw new InvalidParametersException( evaluatedValues );
}
var result = evaluatedValues
.Cast<Value.NumberValue>()
.Select( nv => nv.Number )
.Aggregate( ( acc, v ) => acc * v );
return new Value.NumberValue( result );
}
private static Value.NumberValue PowFunction( IEnvironment environment, Value[] values )
{
if ( values.Length != 2 )
{
throw new InvalidParametersException( values );
}
var baseVal = values[0].Evaluate( environment );
var exponentVal = values[1].Evaluate( environment );
if ( baseVal is not Value.NumberValue baseNum || exponentVal is not Value.NumberValue expNum )
{
throw new InvalidParametersException( [baseVal, exponentVal] );
}
// This can introduce precision artifacts.
return new Value.NumberValue( new decimal( Math.Pow( (double)baseNum.Number, (double)expNum.Number ) ) );
}
private static Value.NumberValue SqrtFunction( IEnvironment environment, Value[] values )
{
if ( values.Length != 1 )
{
throw new InvalidParametersException( values );
}
var val = values[0].Evaluate( environment );
if ( val is not Value.NumberValue numVal || numVal.Number < 0 )
{
throw new InvalidParametersException( [val] );
}
return new Value.NumberValue( new decimal( Math.Sqrt( (double)numVal.Number ) ) );
}
private static Value.FunctionValue DefineFunction( IEnvironment environment, Value[] values )
{
// Expect: (defun function-name (param1 param2 ...) (body))
if ( values.Length != 3 )
{
throw ParamError.Wrong( "defun", "(defun name (params...) body)", values );
}
// Extract function name
var functionName = values[0] switch
{
Value.VariableReferenceValue varRef => varRef.Name,
Value.StringValue strVal => strVal.Text,
_ => throw ParamError.Wrong( "defun", "function name as first parameter", values )
};
// Extract parameter list
if ( values[1] is not Value.ListValue paramList )
{
throw ParamError.Wrong( "defun", "parameter list as second parameter", values );
}
// Extract body
if ( values[2] is not Value.ListValue body )
{
throw ParamError.Wrong( "defun", "function body as third parameter", values );
}
var argNames = paramList.ValueList.Select( p => p switch
{
Value.StringValue stringValue => stringValue.Text,
Value.VariableReferenceValue variableReferenceValue => variableReferenceValue.Name,
_ => throw new InvalidParametersException( [p] )
} ).ToArray();
var functionValue = new Value.FunctionValue( ( env, arglist ) =>
{
if ( arglist.Length != argNames.Length )
{
throw new InvalidParametersException( arglist );
}
var functionEnv = new EnvironmentMap( environment );
for ( var i = 0; i < argNames.Length; i++ )
{
functionEnv.SetVariable( argNames[i], arglist[i].Evaluate( env ) );
}
body.Deconstruct( out var valueList );
return valueList.Execute( functionEnv );
} );
// Register the function in the environment
environment.SetVariable( functionName, functionValue );
return functionValue;
}
private static Value SetFunction( IEnvironment environment, params Value[] values )
{
if ( values is not [Value.VariableReferenceValue, _] )
{
throw ParamError.Wrong( "set", "(set variable value)", values );
}
var value = values[1].Evaluate( environment );
environment.SetVariable( ((Value.VariableReferenceValue)values[0]).Name, value );
return value;
}
private static Value.NumberValue SubtractFunction( IEnvironment environment, params Value[] values )
{
if ( values.Length == 0 )
{
throw new InvalidParametersException( values );
}
var evaluatedValues = new Value[values.Length];
for ( var i = 0; i < values.Length; i++ )
{
evaluatedValues[i] = values[i].Evaluate( environment );
if ( evaluatedValues[i] is not Value.NumberValue )
{
throw new InvalidParametersException( evaluatedValues );
}
}
if ( values.Length == 1 )
{
// Unary minus: negate the single argument
return new Value.NumberValue( -((Value.NumberValue)evaluatedValues[0]).Number );
}
// Binary/n-ary minus: subtract all subsequent values from the first
var result = evaluatedValues
.Cast<Value.NumberValue>()
.Select( nv => nv.Number )
.Aggregate( ( acc, v ) => acc - v );
return new Value.NumberValue( result );
}
private static Value SumFunction( IEnvironment environment, params Value[] values )
{
if ( values.Length == 0 )
{
return new Value.NumberValue( 0 ); // Identity for addition
}
var evaluatedValues = values.Select( v => v.Evaluate( environment ) ).ToArray();
// Check if all are strings
if ( evaluatedValues.All( v => v is Value.StringValue ) )
{
var result = evaluatedValues
.Cast<Value.StringValue>()
.Select( sv => sv.Text )
.Aggregate( ( acc, v ) => acc + v );
return new Value.StringValue( result );
}
// Check if all are numbers
if ( evaluatedValues.All( v => v is Value.NumberValue ) )
{
var result = evaluatedValues.Cast<Value.NumberValue>().Select( nv => nv.Number ).Sum();
return new Value.NumberValue( result );
}
throw new InvalidParametersException( evaluatedValues );
}
private static (decimal a, decimal b) GetTwoNumbers( IEnvironment environment, Value[] values, string opName )
{
if ( values.Length != 2 )
{
throw ParamError.Wrong( opName, "two numbers", values );
}
var v1 = values[0].Evaluate( environment );
var v2 = values[1].Evaluate( environment );
if ( v1 is not Value.NumberValue n1 || v2 is not Value.NumberValue n2 )
{
throw ParamError.Wrong( opName, "two numbers", [v1, v2] );
}
return ( n1.Number, n2.Number );
}
}