Risor Documentation

repository·main·Indexed 21 days ago

https://github.com/deepnoodle-ai/risor

Risor is a fast, embeddable scripting language written in pure Go, designed for safely evaluating user-provided expressions, rules, and small scripts at runtime. It compiles expressions to bytecode for a lightweight VM, offering features such as sandboxed execution, resource limits (max steps, stack depth, and timeouts), and a standard library of built-ins. The ecosystem includes a CLI for executing .risor files, a VS Code extension for syntax highlighting, and an LSP server providing autocompletion, formatting, and definition providers.

Tokens
61.6K
Snippets
224
Records
270
Agent score
74%

What's inside Risor

  1. Choose the appropriate StepMode for your use case

    main

    The StepMode type determines how frequently the OnStep callback is triggered. Choosing the correct mode is critical for performance.

    ModeDescriptionBest Use Case
    StepAllCalls OnStep for every single instruction.Detailed tracing, instruction-level debugging.
    StepNoneNever calls OnStep.Profilers that only need OnCall/OnReturn events.
    StepSampledCalls OnStep every N instructions (defined by SampleInterval).Statistical CPU profiling.
    StepOnLineCalls OnStep only when the source location (file or line) changes.Coverage tools, line-level debugging, breakpoint-based debuggers.
    type StepMode uint8
    
    const (
        StepAll StepMode = iota
        StepNone
        StepSampled
        StepOnLine
    )
  2. Equality rules in Risor

    main

    Equality in Risor is symmetric (a.Equals(b) implies b.Equals(a)).

    Type-specific behavior:

    • Numeric types: Compared by value after converting to a common representation (allows cross-type equality between int, float, and byte).
    • Strings and bytes: bytes can equal string if the content matches.
    • Containers: Uses deep equality.
      • Lists: Must have the same length and element-wise equality.
      • Maps: Must have the same keys and equal values (key order does not matter).
    • Other types: bool (value), null (only to null), time (value), error (message), function/builtin (identity only).
    [1, 2, 3] == [1, 2, 3]          // true - element-wise comparison
    {a: 1, b: 2} == {b: 2, a: 1}    // true - key order doesn't matter
    "hello" == bytes("hello")     // true
  3. How exception handling works in Risor

    main

    Risor uses a Python-like exception model with try, catch, finally, and throw. A key distinction is that try is an expression (Kotlin-style semantics), meaning it evaluates to a value.

    Expression Evaluation Rules

    • Success: The expression evaluates to the value of the try block.
    • Caught Exception: The expression evaluates to the value of the catch block.
    • Finally Block: The finally block always runs but does not affect the expression value of the try or catch blocks.
    // Returns try value on success
    let x = try { 42 } catch (e) { -1 }  // x == 42
    
    // Returns catch value on exception
    let x = try { throw "err"; 42 } catch (e) { -1 }  // x == -1
    
    // Finally runs but doesn't affect result
    let x = try { 42 } finally { 999 }  // x == 42
  4. How AST validation and transformation work together

    main

    Risor uses a two-phase approach for managing script syntax: Validation followed by Transformation.

    1. Validation Phase: Validators check the AST (Abstract Syntax Tree) against specific rules (e.g., restricting certain language features). Validators are non-destructive and can be aggregated to report multiple errors.
    2. Transformation Phase: Transformers modify the AST (e.g., constant folding or dead code removal). Transformers are sequential; each transformer receives the AST produced by the previous one.

    Ordering Principle: Validation always runs before transformation. This ensures the user's original intent is validated against the syntax rules before any automated modifications occur. If you need to validate the result of a transformation, you must manually invoke a validator within your transformer function.

    // Example: Manual post-transform validation
    postValidator := risor.ValidatorFunc(...) 
    
    risor.WithTransform(risor.TransformerFunc(func(p *ast.Program) (*ast.Program, error) {
        transformed := doTransform(p)
        if errs := postValidator.Validate(transformed); len(errs) > 0 {
            return nil, syntax.NewValidationErrors(errs)
        }
        return transformed, nil
    }))
  5. Use `when` and `match` expressions for pattern-based control flow

    main

    Risor provides two primary expressions for declarative control flow: when for condition-based logic and match for pattern-based logic. These are designed for policy evaluation, configuration logic, and data transformation.

    • when Expression: Used for standard conditional logic based on boolean expressions.
    • match Expression: Used for structural pattern matching and variable binding. It allows you to inspect the shape of data and bind parts of that data to local variables.

    To achieve complex validation, you can combine these with:

    • Guards: Arbitrary conditions attached to pattern arms.
    • Schema Validation: Using the matches operator and schema builtin to check types and constraints within a match arm.

    Note: For fallible destructuring (e.g., when a structure might lack expected keys), use match instead of let bindings to handle the failure case safely.

    /* 
    Conceptual usage pattern:
    
    match input {
        UserSchema (if guard_condition) => handle_user(user_vars),
        OrderSchema => handle_order(order_vars),
        _ => handle_default()
    }
    */
  6. Restrict language features using SyntaxConfig

    main

    You can restrict the available Risor language features (e.g., disabling variable declarations, control flow, or function definitions) by providing a SyntaxConfig to the evaluation process. This is useful for creating 'Expression-only' modes or 'Basic Scripting' environments.

    Commonly used presets include:

    • risor.ExpressionOnly: Restricts syntax to literals, operators, variable access, indexing, attribute access, and function calls. Disallows variable declarations, assignments, returns, function definitions, try/catch, if/else, switch, destructuring, spread, and pipes.
    • risor.BasicScripting: Allows variable declarations, assignment, and if/else, but disallows function definitions, error handling, and advanced syntax like destructuring or pipes.
    • risor.FullLanguage: The default behavior (zero value) which allows all features.
    // Example of using a syntax preset to restrict the language
    result, err := risor.Eval(ctx, source,
        risor.WithSyntax(risor.ExpressionOnly),
    )
  7. Ensure exhaustiveness in `when` and `match` expressions

    main

    To prevent undefined behavior, both when and match expressions require a default case to handle all possible inputs. Failing to provide one will result in a compilation error.

    • when requires an else arm.
    • match requires a _ (wildcard) arm.
    // when requires else
    let x = when {
        a => 1
        // Error: missing else arm
    }
    
    // match requires _ (wildcard)
    let y = match value {
        1 => "one"
        // Error: missing default arm
    }
  8. What Risor is and isn't

    main

    What Risor is

    A fast, embeddable scripting language for Go applications. It compiles expressions to bytecode and runs on a lightweight VM. It is intended for evaluating user-provided expressions, rules, or small scripts safely at runtime.

    What Risor isn't

    • It is not a general-purpose programming language meant to replace Python or TypeScript.
    • It has no package manager, no module imports, and no third-party ecosystem by design.
    • Extension is handled via Go code (adding builtins and passing data) rather than a script-side ecosystem.
  9. Understand error wrapping and equality in Risor v2

    main

    In Risor v2, error handling currently relies on Go-specific implementation details that are not part of the stable scripting API:

    • Error Wrapping: The error() builtin uses fmt.Errorf internally. This allows scripts to use the %w verb to create wrapped error chains.
    • Error Equality: The *Error.Equals method (and the == operator) uses errors.Is logic (available since v2.2.0). This means a wrapped sentinel error will match its descendants when compared using ==.

    Warning: Because these behaviors are implementation details and not documented in the language reference, they are subject to change in future versions (v3) and should not be relied upon for stable script logic.