Skip to content

Latest commit

 

History

History
340 lines (252 loc) · 11.4 KB

ual-1.4-DESIGN-type-system.md

File metadata and controls

340 lines (252 loc) · 11.4 KB

ual's bring_<type> System: A Superior Approach to Type Conversion

Introduction

Type conversion is a fundamental operation in programming languages, but traditional approaches often lead to subtle bugs, security vulnerabilities, and unpredictable behavior. Functions like C's atoi() or Go's strconv package provide type conversion capabilities, but their behavior can be inconsistent, error-prone, and difficult to reason about.

ual's bring_<type> operation represents a principled alternative that addresses these issues through a carefully designed approach to type conversion. This document examines the four key aspects of this system and how they eliminate common problems found in other languages.

1. Explicit Intent

The Problem with Traditional Approaches

In many languages, type conversions can happen implicitly:

// JavaScript
let value = "42" + 10;  // Results in "4210" (string concatenation)
let anotherValue = "42" - 10;  // Results in 32 (numeric subtraction)

Even with explicit conversion functions, the intention isn't always clear:

// C
int value = atoi(input_string);  // What happens if input_string is invalid?

ual's Solution: Explicitly Requested Conversions

In ual, conversions must be explicitly requested through the appropriate bring_<type> method:

-- ual
@string_stack: push("42")
@integer_stack: bring_string(string_stack.pop())  -- Explicitly convert string to integer

This explicit approach has several benefits:

  1. Clear programmer intent: The code explicitly states the intention to convert from one type to another
  2. Self-documenting code: Reading the code immediately reveals what type conversions are happening
  3. Consistent mental model: Developers always know exactly how type conversions occur
  4. Visibility in code reviews: Type conversions are easily identified during code review

ual's typed stacks make the source and destination types explicit, eliminating ambiguity about what conversion is being performed. The operation's name - bring_string, bring_integer, etc. - clearly communicates both the source type and the intended destination type.

2. Type-Specific Conversion Logic

The Problem with Traditional Approaches

Many languages use generic conversion functions with unpredictable behavior:

// C
// What happens with atoi("42hello")? Or atoi("")?
int val1 = atoi("42hello");  // Returns 42, silently ignoring "hello"
int val2 = atoi("");         // Returns 0, which could be valid input or error
// Go
// Many possible ways to convert, each with different behavior
i, err := strconv.Atoi("42")
i64, err := strconv.ParseInt("42", 10, 64)
f, err := strconv.ParseFloat("42.5", 64)

ual's Solution: Type-Specific Conversion Paths

ual implements each conversion path with specific, well-defined logic tailored to the source and destination types:

-- ual
-- Integer to Float conversion - well-defined, preserves numeric value
@integer_stack: push(42)
@float_stack: bring_integer(integer_stack.pop())  -- Converts to 42.0

-- String to Integer conversion - well-defined parsing rules
@string_stack: push("42")
@integer_stack: bring_string(string_stack.pop())  -- Parses to integer 42

-- String to Float conversion - different parsing logic for floating-point
@string_stack: push("42.5")
@float_stack: bring_string(string_stack.pop())  -- Parses to float 42.5

Each conversion path in ual has:

  1. Specialized parsing/conversion logic: Type-specific rules for how conversions work
  2. Consistent behavior: Well-defined handling of edge cases
  3. Appropriate error generation: Type-specific error messages when conversion fails
  4. Optimization opportunities: Each conversion path can be implemented efficiently

By having distinct methods for each conversion type, ual ensures that the behavior is specifically designed for that particular conversion, rather than trying to handle all conversions through generic functions.

3. Controlled Conversion Paths

The Problem with Traditional Approaches

Many languages allow arbitrary type casting, opening the door to undefined behavior:

// C
float f = 3.14;
int* ptr = (int*)&f;  // Reinterprets float bits as integer pointer - dangerous!
*ptr = 42;            // Undefined behavior

Even in safer languages, the proliferation of conversion functions can lead to confusion:

# Python - many ways to convert, with different behaviors
int("42")        # 42
float("42")      # 42.0
int("42.5")      # ValueError
int(float("42.5")) # 42 (truncates)
round(float("42.5")) # 42 (rounds)

ual's Solution: Only Defined Conversions Are Possible

ual only permits conversions that have been explicitly defined in the type system:

-- ual
-- These conversions are defined and have clear semantics
@string_stack: push("42")
@integer_stack: bring_string(string_stack.pop())  -- Defined conversion

@integer_stack: push(42)
@float_stack: bring_integer(integer_stack.pop())  -- Defined conversion

-- If a conversion isn't defined, it's not possible:
-- Hypothetical undefined conversion would be a compile-time error
-- @complex_number_stack: bring_string(string_stack.pop())  -- If not defined, not allowed

This controlled approach offers:

  1. Compile-time safety: Many invalid conversions are caught at compile time
  2. Well-defined semantics: Each permitted conversion has clear, documented behavior
  3. No undefined behavior: Impossible to perform arbitrary type reinterpretation
  4. Extensibility: New conversion paths can be added in a controlled manner

By restricting conversions to only those that are explicitly defined, ual prevents a whole class of bugs related to inappropriate or dangerous type conversions.

4. Atomic Operations

The Problem with Traditional Approaches

Traditional conversion approaches often require multiple steps, which can lead to inconsistent state if errors occur:

// C
char* input = get_user_input();
int value;
// Multiple steps that can fail at different points:
if (is_valid_number(input)) {
    value = atoi(input);
    // Use value...
} else {
    // Handle error...
}

These multi-step processes create opportunities for bugs:

  • Forgetting to check for errors
  • Checking for errors incorrectly
  • Leaving data structures in inconsistent states when errors occur

ual's Solution: Atomic Pop/Convert/Push

ual's bring_<type> operation combines three operations (pop, convert, push) into a single atomic step:

-- ual
-- Single atomic operation
@float_stack: bring_string(string_stack.pop())

-- If the conversion succeeds, value is on float_stack
-- If the conversion fails, nothing is on float_stack and value is no longer on string_stack

The atomicity guarantee provides:

  1. Consistent stack state: Either the operation fully succeeds or fully fails
  2. No partial completion: Impossible to have "half-converted" values
  3. Clean error handling: Conversion errors can be handled uniformly
  4. Interrupt safety: Even if interrupted, stacks remain in a consistent state
  5. Thread safety: In multi-threaded contexts, atomic operations prevent race conditions

This atomic behavior is particularly important in embedded systems, where maintaining system consistency is critical and resources for error recovery may be limited.

How This Eliminates Common Type-Related Bugs

ual's approach to type conversion effectively eliminates several classes of bugs that plague other languages:

1. No Implicit Coercions

Problem in other languages:

// JavaScript
"5" + 10 // "510" - string concatenation
"5" - 10 // -5 - numeric subtraction

ual's solution: All conversions must be explicit, preventing unexpected coercion behavior.

2. No Type Confusion

Problem in other languages:

// C
void* generic_data = get_data();
int* data_as_int = (int*)generic_data;  // Is this actually an int?

ual's solution: Types are attached to stacks, not individual values, and conversions are explicit operations that validate type compatibility.

3. No Undefined Behavior from Invalid Casts

Problem in other languages:

// C
float f = 3.14;
int i = *(int*)&f;  // Reinterprets bit pattern - undefined behavior

ual's solution: Only defined conversions with well-specified behavior are permitted, preventing undefined behavior.

4. No Silent Truncation or Data Loss

Problem in other languages:

// C
double d = 1234567890.123;
int i = (int)d;  // Silently truncates to 1234567890, losing precision

ual's solution: Conversions that might lose information are explicit, making the potential for data loss visible.

5. No Forgotten Error Handling

Problem in other languages:

// Go
i, err := strconv.Atoi(input)
// Easy to forget to check 'err'

ual's solution: The atomic nature of bring_<type> operations makes error handling more consistent, as the operation either fully succeeds or fully fails.

Practical Example: Parsing User Input

Let's compare a typical input parsing scenario across different approaches:

Traditional C Approach:

char* input = get_user_input();
int value;
char* endptr;

// Multiple steps, error-prone
value = strtol(input, &endptr, 10);
if (*endptr != '\0') {
    // Conversion error - didn't consume whole string
    handle_error();
} else if (errno == ERANGE) {
    // Range error
    handle_range_error();
} else {
    // Success - use value
    process_value(value);
}

Go Approach:

input := getUserInput()
value, err := strconv.Atoi(input)
if err != nil {
    // Handle error
    handleError(err)
} else {
    // Success
    processValue(value)
}

ual Approach:

@string_stack: push(get_user_input())

-- Try conversion with atomicity guarantees
success, err = pcall(function()
    @integer_stack: bring_string(string_stack.pop())
end)

if success then
    process_value(integer_stack.pop())
else
    handle_error(err)
end

-- Or more elegantly with .consider pattern:
function parse_input()
    @string_stack: push(get_user_input())
    
    result = {}
    pcall(function()
        @integer_stack: bring_string(string_stack.pop())
        result.Ok = integer_stack.pop()
    end, function(err)
        result.Err = err
    end)
    
    return result
end

-- Usage:
parse_input().consider {
    if_ok  process_value(_1)
    if_err handle_error(_1)
}

The ual approach offers better guarantees about the state of the system after conversion attempts, as well as a more consistent error handling pattern.

Conclusion

ual's bring_<type> system represents a principled approach to type conversion that addresses many of the shortcomings found in traditional programming languages. By requiring explicit conversion intent, implementing type-specific conversion logic, controlling available conversion paths, and ensuring atomicity, ual eliminates a whole class of bugs related to type manipulation.

This approach is particularly valuable in embedded systems programming, where reliability and predictability are paramount concerns. However, the lessons from this design could benefit programming languages across domains, as type conversion issues are universal problems in software development.

The system demonstrates how careful language design can eliminate entire categories of errors by making the right choices easy and the wrong choices impossible. By prioritizing explicit intent and well-defined behavior over convenience and implicit conversions, ual creates a more robust foundation for reliable software development.