Skip to content

Type System

The expression language implements a robust type system that handles various data types and provides automatic type conversion where appropriate. This document provides a comprehensive overview of the type system implementation.

1. Supported Types

1.1 Primitive Types

String

  • Represented as double or single quoted text: "example" or 'example'
  • Used for text comparisons and string operations
  • Common operations: ==, !=, starts, ends, includes, match

Number

  • Supports both integers and floating-point numbers
  • All numbers are internally converted to float64 for consistent comparison
  • Examples: 42, 3.14, -1, 1.0
  • Automatic type conversion between integer and float types
  • Used in numeric comparisons: >, <, >=, <=

Boolean

  • Represents true/false values
  • Result type of all comparison operations
  • Used in logical operations (and, or, not)
  • Result type of boolean functions

1.2 Complex Types

Arrays

  • Accessed using square bracket notation: array[0]
  • Zero-based indexing
  • Supports nested access: users[0].name
  • Returns nil for out-of-bounds access without error

Objects (Maps)

  • Accessed using dot notation: user.name
  • Supports nested access: user.address.city
  • Returns nil for missing fields without error
  • Keys are always strings

2. Type Conversion

2.1 Numeric Conversion

  • Automatic conversion between integer and float types
  • All numeric values are converted to float64 for comparison
  • Conversion happens in these cases:
    • Field access (integers are converted to float64)
    • Numeric comparisons
    • Function arguments expecting numbers

2.2 Type Coercion Rules

  • No implicit conversion between strings and numbers
  • Boolean conversion follows "truthy" rules:
    • Empty string = false
    • Zero = false
    • nil = false
    • Empty arrays/objects = false
    • All other values = true switch v := val.(type) { case bool: return v case string: return len(v) > 0 case float64: return v != 0 case int: return v != 0 case []interface{}: return len(v) > 0 case map[string]interface{}: return len(v) > 0 default: return false
go
// Truthy evaluation rules
func isTruthy(val interface{}) bool {
    if val == nil {
        return false
    }

    switch v := val.(type) {
    case bool:
        return v
    case string:
        return v != ""
    case float64:
        return v != 0
    case int:
        return v != 0
    case []interface{}:
        return len(v) > 0
    case map[string]interface{}:
        return len(v) > 0
    default:
        return false
    }
}

3. Type Comparison

3.1 Equality Comparison

  • Strict equality comparison using == and !=
  • Type-aware comparison that considers:
    • Primitive type equality
    • Deep equality for complex types
    • nil value handling
    • Numeric type conversion
go
// Example equality comparison
func isEqual(a, b interface{}) bool {
    // Handle nil cases
    if a == nil || b == nil {
        return a == b
    }

    // Type-specific comparisons
    switch va := a.(type) {
    case float64:
        if vb, ok := b.(float64); ok {
            return va == vb
        }
        if vb, ok := b.(int); ok {
            return va == float64(vb)
        }
    case string:
        if vb, ok := b.(string); ok {
            return va == vb
        }
    case bool:
        if vb, ok := b.(bool); ok {
            return va == vb
        }
    }

    return reflect.DeepEqual(a, b)
}

3.2 Numeric Comparison

  • Supports >, <, >=, <= operators
  • Automatic type conversion to float64
  • Error handling for non-numeric comparisons
go
// Example numeric comparison
func compareNumeric(a, b interface{}, op string) (bool, error) {
    var aVal, bVal float64

    // Convert first value to float64
    switch va := a.(type) {
    case float64:
        aVal = va
    case int:
        aVal = float64(va)
    default:
        return false, fmt.Errorf("cannot compare non-numeric type: %T", a)
    }

    // Convert second value to float64
    switch vb := b.(type) {
    case float64:
        bVal = vb
    case int:
        bVal = float64(vb)
    default:
        return false, fmt.Errorf("cannot compare non-numeric type: %T", b)
    }

    // Perform comparison based on operator
    switch op {
    case string(GT):
        return aVal > bVal, nil
    case string(LT):
        return aVal < bVal, nil
    case string(GTE):
        return aVal >= bVal, nil
    case string(LTE):
        return aVal <= bVal, nil
    default:
        return false, fmt.Errorf("unknown comparison operator: %s", op)
    }
}

4. Error Handling

4.1 Type Errors

  • Invalid type comparisons (e.g., comparing string with number)
  • Non-numeric values in numeric comparisons
  • Invalid array index types
  • Missing field access on nil values

4.2 Graceful Handling

  • Missing field access returns nil without error
  • Out-of-bounds array access returns nil without error
  • Invalid type access returns nil without error

5. Best Practices

  1. Always use appropriate comparison operators for the data type:

    • Use ==, != for all types
    • Use >, <, >=, <= only for numbers
    • Use string operations (starts, ends, includes, match) for strings
  2. Be aware of automatic numeric conversion:

    // These are equivalent due to automatic conversion
    value == 42
    value == 42.0
  3. Use explicit type checking in functions when needed:

    // Example function implementation
    if val, ok := value.(float64); ok {
        // Handle float64 case
    } else if val, ok := value.(int); ok {
        // Handle int case
    }
  4. Handle nil values appropriately:

    // Check for nil before operations
    if value != nil {
        // Perform operations
    }
  5. Use proper error handling for type-related operations:

    if result, err := compareNumeric(val1, val2, ">"); err != nil {
        // Handle type error
    } else {
        // Use result
    }