diff --git a/backend/src/BuiltinExecution/Libs/Reflection.fs b/backend/src/BuiltinExecution/Libs/Reflection.fs index 957998ded1..7985e806e0 100644 --- a/backend/src/BuiltinExecution/Libs/Reflection.fs +++ b/backend/src/BuiltinExecution/Libs/Reflection.fs @@ -28,6 +28,134 @@ let fns : List = | _ -> incorrectArgs () sqlSpec = NotQueryable previewable = Pure + deprecated = NotDeprecated } + + + { name = fn "reflectType" 0 + typeParams = [] + parameters = [ Param.make "dv" (TVariable "a") "" ] + returnType = + TCustomType( + Ok( + FQTypeName.fqPackage + LibExecution.PackageIDs.Type.LanguageTools.RuntimeTypes.valueType + ), + [] + ) + description = + "Returns the ValueType of a value without the full Dval structure. More lightweight than reflect for type inspection." + fn = + function + | _, _, _, [ dv ] -> + dv + |> Dval.toValueType + |> LibExecution.RuntimeTypesToDarkTypes.ValueType.toDT + |> Ply + | _ -> incorrectArgs () + sqlSpec = NotQueryable + previewable = Pure + deprecated = NotDeprecated } + + + { name = fn "typeOf" 0 + typeParams = [] + parameters = [ Param.make "dv" (TVariable "a") "" ] + returnType = TString + description = + "Returns a human-readable type name as a string (e.g., 'Int64', 'List', 'Dict'). Lightweight alternative to full reflection." + fn = + function + | _, _, _, [ dv ] -> + let rec typeToString (vt : ValueType) : string = + match vt with + | ValueType.Unknown -> "Unknown" + | ValueType.Known kt -> + match kt with + | KTUnit -> "Unit" + | KTBool -> "Bool" + | KTInt8 -> "Int8" + | KTUInt8 -> "UInt8" + | KTInt16 -> "Int16" + | KTUInt16 -> "UInt16" + | KTInt32 -> "Int32" + | KTUInt32 -> "UInt32" + | KTInt64 -> "Int64" + | KTUInt64 -> "UInt64" + | KTInt128 -> "Int128" + | KTUInt128 -> "UInt128" + | KTFloat -> "Float" + | KTChar -> "Char" + | KTString -> "String" + | KTUuid -> "Uuid" + | KTDateTime -> "DateTime" + | KTList inner -> $"List<{typeToString inner}>" + | KTDict inner -> $"Dict<{typeToString inner}>" + | KTTuple(first, second, theRest) -> + let types = + [ first; second ] @ theRest |> List.map typeToString |> String.concat ", " + $"({types})" + | KTCustomType(typeName, typeArgs) -> + let name = + match typeName with + | FQTypeName.Package id -> $"Package({id})" + let args = + if List.isEmpty typeArgs then + "" + else + "<" + (typeArgs |> List.map typeToString |> String.concat ", ") + ">" + name + args + | KTFn(args, ret) -> + let argStr = + args + |> NEList.toList + |> List.map typeToString + |> String.concat ", " + $"({argStr}) -> {typeToString ret}" + | KTDB inner -> $"DB<{typeToString inner}>" + + dv |> Dval.toValueType |> typeToString |> DString |> Ply + | _ -> incorrectArgs () + sqlSpec = NotQueryable + previewable = Pure + deprecated = NotDeprecated } + + + { name = fn "typeName" 0 + typeParams = [] + parameters = [ Param.make "dv" (TVariable "a") "" ] + returnType = + TCustomType( + Ok(FQTypeName.stdlib "Option" 0), + [ TCustomType( + Ok( + FQTypeName.fqPackage + LibExecution.PackageIDs.Type.LanguageTools.RuntimeTypes.FQTypeName + .fqTypeName + ), + [] + ) ] + ) + description = + "Returns the FQTypeName for custom types (records and enums). Returns None for primitives and built-in types." + fn = + function + | _, _, _, [ dv ] -> + let typeName = + match dv with + | DRecord(_, typeName, _, _) -> Some typeName + | DEnum(_, typeName, _, _, _) -> Some typeName + | _ -> None + + match typeName with + | Some name -> + LibExecution.RuntimeTypesToDarkTypes.FQTypeName.toDT name + |> Dval.optionSome (VT.known LibExecution.RuntimeTypesToDarkTypes.FQTypeName.knownType) + | None -> + Dval.optionNone (VT.known LibExecution.RuntimeTypesToDarkTypes.FQTypeName.knownType) + |> Ply + | _ -> incorrectArgs () + sqlSpec = NotQueryable + previewable = Pure deprecated = NotDeprecated } ] let builtins = LibExecution.Builtin.make [] fns diff --git a/backend/testfiles/execution/cli/reflection-test.dark b/backend/testfiles/execution/cli/reflection-test.dark new file mode 100644 index 0000000000..26aaafeb4a --- /dev/null +++ b/backend/testfiles/execution/cli/reflection-test.dark @@ -0,0 +1,46 @@ +// CLI test for reflection capabilities +// This demonstrates using reflection in CLI scripts + +module ReflectionCliTest = + type Person = { name: String; age: Int64 } + + type Status = + | Active + | Inactive of String + + let testValue = Person { name = "Alice"; age = 30L } + + let testEnum = Status.Active + + // Test typeOf + let typeOfTest = + (Builtin.typeOf testValue |> Stdlib.String.startsWith "Package") + && (Builtin.typeOf 42L == "Int64") + && (Builtin.typeOf "hello" == "String") + + // Test reflectType + let reflectTypeTest = + match Builtin.reflectType 42L with + | Known kt -> + (match kt with + | KTInt64 -> true + | _ -> false) + | _ -> false + + // Test typeName + let typeNameTest = + match Builtin.typeName testValue with + | Some _fqtn -> true + | None -> false + + // Test package functions + let packageFnTests = + Darklang.LanguageTools.Reflection.isPrimitive 42L + && Darklang.LanguageTools.Reflection.isCustomType testValue + && Darklang.LanguageTools.Reflection.isCollection [ 1L; 2L ] + + // Return 0 for success if all tests pass + (if typeOfTest && reflectTypeTest && typeNameTest && packageFnTests then + 0L + else + 1L) diff --git a/backend/testfiles/execution/stdlib/reflection.dark b/backend/testfiles/execution/stdlib/reflection.dark new file mode 100644 index 0000000000..644f62f167 --- /dev/null +++ b/backend/testfiles/execution/stdlib/reflection.dark @@ -0,0 +1,82 @@ +// Tests for reflection builtins + +// Test Builtin.typeOf with primitives +Builtin.typeOf () = "Unit" +Builtin.typeOf true = "Bool" +Builtin.typeOf 42L = "Int64" +Builtin.typeOf 3.14 = "Float" +Builtin.typeOf "hello" = "String" +Builtin.typeOf 'c' = "Char" + +// Test Builtin.typeOf with collections +Builtin.typeOf [ 1L; 2L; 3L ] = "List" +Builtin.typeOf [ "a"; "b" ] = "List" +Builtin.typeOf (Stdlib.Dict.empty ()) = "Dict" + +// Test Builtin.typeOf with tuples +Builtin.typeOf (1L, "hello") = "(Int64, String)" +Builtin.typeOf (1L, "hello", true) = "(Int64, String, Bool)" + +// Test Builtin.reflectType with primitives +(match Builtin.reflectType 42L with +| Known kt -> + (match kt with + | KTInt64 -> true + | _ -> false) +| _ -> false) = true + +(match Builtin.reflectType "hello" with +| Known kt -> + (match kt with + | KTString -> true + | _ -> false) +| _ -> false) = true + +(match Builtin.reflectType true with +| Known kt -> + (match kt with + | KTBool -> true + | _ -> false) +| _ -> false) = true + +// Test Builtin.reflectType with list +(match Builtin.reflectType [ 1L; 2L; 3L ] with +| Known kt -> + (match kt with + | KTList _elementType -> true + | _ -> false) +| _ -> false) = true + +// Test Builtin.typeName with primitives (should return None) +Builtin.typeName 42L = Stdlib.Option.Option.None + +Builtin.typeName "hello" = Stdlib.Option.Option.None + +Builtin.typeName [ 1L; 2L ] = Stdlib.Option.Option.None + +// Test Builtin.typeName with custom types +module CustomTypeTests = + type Person = { name: String; age: Int64 } + + type Color = + | Red + | Blue + | Green of String + + let person = Person { name = "Alice"; age = 30L } + + let color = Color.Red + + // typeName should return Some for custom types + (match Builtin.typeName person with + | Some _fqTypeName -> true + | None -> false) = true + + (match Builtin.typeName color with + | Some _fqTypeName -> true + | None -> false) = true + + // typeOf should work with custom types + (Builtin.typeOf person |> Stdlib.String.startsWith "Package") = true + + (Builtin.typeOf color |> Stdlib.String.startsWith "Package") = true diff --git a/packages/darklang/languageTools/reflection-demo.dark b/packages/darklang/languageTools/reflection-demo.dark new file mode 100644 index 0000000000..7100b34027 --- /dev/null +++ b/packages/darklang/languageTools/reflection-demo.dark @@ -0,0 +1,150 @@ +module Darklang.LanguageTools.ReflectionDemo + +// This module demonstrates practical uses of Darklang's reflection capabilities +// It shows how reflection can be used for debugging, validation, serialization, and more + +// +type Dval = Darklang.LanguageTools.RuntimeTypes.Dval +type ValueType = Darklang.LanguageTools.RuntimeTypes.ValueType +type Option = Stdlib.Option.Option +type Result = Stdlib.Result.Result +// + + +/// Example 1: Type-aware pretty printing +/// This function uses reflection to print values in a more readable way +let prettyPrint (value: Dval) : String = + let typeName = Builtin.typeOf value + + match value with + | DUnit -> "()" + | DBool b -> + (if b then + "true" + else + "false") + | DString s -> "\"" ++ s ++ "\"" + | DInt64 i -> Stdlib.Int64.toString i ++ "L" + | DFloat f -> Stdlib.Float.toString f + | DList(_vt, items) -> + let count = Stdlib.List.length items + + typeName + ++ " with " + ++ (count |> Stdlib.Int64.toString) + ++ " items" + | DDict(_vt, entries) -> + let count = (Stdlib.Dict.keys entries) |> Stdlib.List.length + + typeName + ++ " with " + ++ (count |> Stdlib.Int64.toString) + ++ " entries" + | DRecord(_rt, _st, _ta, fields) -> + let fieldCount = (Stdlib.Dict.keys fields) |> Stdlib.List.length + + typeName + ++ " record with " + ++ (fieldCount |> Stdlib.Int64.toString) + ++ " fields" + | DEnum(_rt, _st, _ta, caseName, _fields) -> + typeName ++ "::" ++ caseName + | _ -> typeName + + +/// Example 2: Validate that a value matches expected type +/// This is useful for runtime type checking and validation +let validateType (value: Dval) (expectedTypeName: String) : Bool = + let actualTypeName = Builtin.typeOf value + actualTypeName == expectedTypeName + + +/// Example 3: Check if a value is serializable +/// Some types like functions cannot be easily serialized +let isSerializable (value: Dval) : Bool = + let isFunc = Darklang.LanguageTools.Reflection.isFunction value + not isFunc + + +/// Example 4: Extract metadata from custom types +/// This function shows how to introspect records and enums +let extractMetadata (value: Dval) : String = + if Darklang.LanguageTools.Reflection.isRecord value then + match Darklang.LanguageTools.Reflection.getFields value with + | Some fields -> + let fieldNames = Stdlib.Dict.keys fields + + "Record with fields: " + ++ Stdlib.String.join fieldNames ", " + | None -> "Record (no fields)" + else if Darklang.LanguageTools.Reflection.isEnum value then + match Darklang.LanguageTools.Reflection.getCaseName value with + | Some caseName -> "Enum case: " ++ caseName + | None -> "Enum (unknown case)" + else if Darklang.LanguageTools.Reflection.isList value then + match Darklang.LanguageTools.Reflection.getListElementType value with + | Some elementType -> + "List of " ++ (Builtin.typeOf (DList(elementType, []))) + | None -> "List (unknown element type)" + else if Darklang.LanguageTools.Reflection.isDict value then + match Darklang.LanguageTools.Reflection.getDictValueType value with + | Some valueType -> + "Dict with values of type " ++ (Builtin.typeOf (DDict(valueType, Dict {}))) + | None -> "Dict (unknown value type)" + else + "Primitive or other type" + + +/// Example 5: Count values by type in a heterogeneous list +/// This demonstrates using reflection for type-based analysis +let countByType (values: List) : Dict = + Stdlib.List.fold + values + (Dict {}) + (fun acc value -> + let typeName = Builtin.typeOf value + + let currentCount = + match Stdlib.Dict.get acc typeName with + | Some count -> count + | None -> 0L + + Stdlib.Dict.set acc typeName (currentCount + 1L)) + + +/// Example 6: Filter list items by type +/// Get all items of a specific type from a heterogeneous collection +let filterByType (values: List) (targetType: String) : List = + Stdlib.List.filter values (fun value -> (Builtin.typeOf value) == targetType) + + +/// Example 7: Type-based routing/dispatch +/// Route values to different handlers based on their type +let routeByType (value: Dval) : String = + if Darklang.LanguageTools.Reflection.isPrimitive value then + "route to primitive handler" + else if Darklang.LanguageTools.Reflection.isCollection value then + "route to collection handler" + else if Darklang.LanguageTools.Reflection.isCustomType value then + "route to custom type handler" + else if Darklang.LanguageTools.Reflection.isFunction value then + "route to function handler" + else + "route to unknown handler" + + +/// Example 8: Schema inference for collections +/// Infer the schema of a list by examining its elements +let inferListSchema (values: List) : String = + if Stdlib.List.isEmpty values then + "Empty list - no schema" + else + let types = + values + |> Stdlib.List.map (fun v -> Builtin.typeOf v) + |> Stdlib.List.unique + + if (Stdlib.List.length types) == 1L then + "Homogeneous list of " ++ ((Stdlib.List.head types) |> Builtin.unwrap) + else + "Heterogeneous list with types: " ++ Stdlib.String.join types ", " diff --git a/packages/darklang/languageTools/reflection.dark b/packages/darklang/languageTools/reflection.dark new file mode 100644 index 0000000000..545c0c518c --- /dev/null +++ b/packages/darklang/languageTools/reflection.dark @@ -0,0 +1,131 @@ +module Darklang.LanguageTools.Reflection + +// +type Dval = Darklang.LanguageTools.RuntimeTypes.Dval +type ValueType = Darklang.LanguageTools.RuntimeTypes.ValueType +type KnownType = Darklang.LanguageTools.RuntimeTypes.KnownType +type DvalMap = Darklang.LanguageTools.RuntimeTypes.DvalMap +type Option = Stdlib.Option.Option +// + + +/// Check if a Dval is a primitive type (Unit, Bool, Int*, UInt*, Float, Char, String, DateTime, Uuid) +let isPrimitive (dv: Dval) : Bool = + match dv with + | DUnit -> true + | DBool _ -> true + | DInt8 _ -> true + | DUInt8 _ -> true + | DInt16 _ -> true + | DUInt16 _ -> true + | DInt32 _ -> true + | DUInt32 _ -> true + | DInt64 _ -> true + | DUInt64 _ -> true + | DInt128 _ -> true + | DUInt128 _ -> true + | DFloat _ -> true + | DChar _ -> true + | DString _ -> true + | DDateTime _ -> true + | DUuid _ -> true + | _ -> false + + +/// Check if a Dval is a collection type (List, Dict, or Tuple) +let isCollection (dv: Dval) : Bool = + match dv with + | DList _ -> true + | DDict _ -> true + | DTuple _ -> true + | _ -> false + + +/// Check if a Dval is a custom type (Record or Enum) +let isCustomType (dv: Dval) : Bool = + match dv with + | DRecord _ -> true + | DEnum _ -> true + | _ -> false + + +/// Check if a Dval is a function (DApplicable) +let isFunction (dv: Dval) : Bool = + match dv with + | DApplicable _ -> true + | _ -> false + + +/// Get the type name of a Dval as a human-readable string +let getTypeName (dv: Dval) : String = + Builtin.typeOf dv + + +/// Extract record fields if the Dval is a record, otherwise return None +let getFields (dv: Dval) : Option = + match dv with + | DRecord(_runtimeTypeName, _sourceTypeName, _typeArgs, fields) -> + Stdlib.Option.Option.Some fields + | _ -> Stdlib.Option.Option.None + + +/// Extract enum case name if the Dval is an enum, otherwise return None +let getCaseName (dv: Dval) : Option = + match dv with + | DEnum(_runtimeTypeName, _sourceTypeName, _typeArgs, caseName, _fields) -> + Stdlib.Option.Option.Some caseName + | _ -> Stdlib.Option.Option.None + + +/// Extract List element type if the Dval is a List, otherwise return None +let getListElementType (dv: Dval) : Option = + match dv with + | DList(elementType, _items) -> Stdlib.Option.Option.Some elementType + | _ -> Stdlib.Option.Option.None + + +/// Extract Dict value type if the Dval is a Dict, otherwise return None +let getDictValueType (dv: Dval) : Option = + match dv with + | DDict(valueType, _entries) -> Stdlib.Option.Option.Some valueType + | _ -> Stdlib.Option.Option.None + + +/// Get the full ValueType of a Dval (wrapper around Builtin.reflectType) +let getValueType (dv: Dval) : ValueType = + Builtin.reflectType dv + + +/// Check if a Dval is a List type +let isList (dv: Dval) : Bool = + match dv with + | DList _ -> true + | _ -> false + + +/// Check if a Dval is a Dict type +let isDict (dv: Dval) : Bool = + match dv with + | DDict _ -> true + | _ -> false + + +/// Check if a Dval is a Tuple type +let isTuple (dv: Dval) : Bool = + match dv with + | DTuple _ -> true + | _ -> false + + +/// Check if a Dval is a Record type +let isRecord (dv: Dval) : Bool = + match dv with + | DRecord _ -> true + | _ -> false + + +/// Check if a Dval is an Enum type +let isEnum (dv: Dval) : Bool = + match dv with + | DEnum _ -> true + | _ -> false