ferricel-core 0.1.3

Core compiler and runtime library for ferricel (CEL → Wasm)
Documentation
use cel::common::ast::CallExpr;
use ferricel_types::functions::RuntimeFunction;
use walrus::{InstrSeqBuilder, LocalId, ValType};

use crate::compiler::{
    context::{CompilerContext, CompilerEnv},
    expr::compile_expr,
};

/// Returns the single memory id from the module, or an error if none exists.
pub fn get_memory_id(module: &walrus::Module) -> Result<walrus::MemoryId, anyhow::Error> {
    module
        .memories
        .iter()
        .next()
        .ok_or_else(|| anyhow::anyhow!("No memory found"))
        .map(|m| m.id())
}

/// Helper function to compile a string literal into a CelValue and store it in a local.
/// Returns the LocalId containing the pointer to the CelValue::String.
///
/// This is used for struct field names and type names to avoid code duplication.
pub fn compile_string_to_local(
    s: &str,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    module: &mut walrus::Module,
) -> Result<LocalId, anyhow::Error> {
    let bytes = s.as_bytes();
    let len = bytes.len() as i32;

    // Allocate memory for the string data
    let data_ptr_local = module.locals.add(ValType::I32);
    body.i32_const(len)
        .call(env.get(RuntimeFunction::Malloc))
        .local_set(data_ptr_local);

    // Get memory reference
    let memory_id = get_memory_id(module)?;

    // Write each byte of the string to the allocated memory
    for (offset, &byte) in bytes.iter().enumerate() {
        body.local_get(data_ptr_local);
        body.i32_const(byte as i32);
        body.store(
            memory_id,
            walrus::ir::StoreKind::I32_8 { atomic: false },
            walrus::ir::MemArg {
                align: 1,
                offset: offset as u64,
            },
        );
    }

    // Call cel_create_string(data_ptr, len)
    body.local_get(data_ptr_local);
    body.i32_const(len);
    body.call(env.get(RuntimeFunction::CreateString));

    // Store the resulting CelValue pointer in a local
    let result_local = module.locals.add(ValType::I32);
    body.local_set(result_local);

    Ok(result_local)
}

/// Emit instructions to write a compile-time string constant into Wasm memory
/// and leave `(ptr: i32, len: i32)` on the stack.
///
/// When `s` is empty, pushes `(0i32, 0i32)` without any allocation.
pub fn emit_string_const(
    s: &str,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    memory_id: walrus::MemoryId,
    module: &mut walrus::Module,
) {
    let bytes = s.as_bytes();
    let len = bytes.len() as i32;
    if len == 0 {
        body.i32_const(0);
        body.i32_const(0);
        return;
    }
    let ptr_local = module.locals.add(ValType::I32);
    body.i32_const(len)
        .call(env.get(RuntimeFunction::Malloc))
        .local_set(ptr_local);
    for (offset, &byte) in bytes.iter().enumerate() {
        body.local_get(ptr_local);
        body.i32_const(byte as i32);
        body.store(
            memory_id,
            walrus::ir::StoreKind::I32_8 { atomic: false },
            walrus::ir::MemArg {
                align: 1,
                offset: offset as u64,
            },
        );
    }
    body.local_get(ptr_local);
    body.i32_const(len);
}

/// Compile a method/function call that takes one receiver (no extra arguments).
///
/// - Method style:   `receiver.fn()`  — `target` is `Some`, `args` is empty
/// - Function style: `fn(receiver)`   — `target` is `None`, `args` has 1 element
///
/// Compiles the receiver expression, then emits `call runtime_fn`.
pub fn compile_call_unary(
    call_expr: &CallExpr,
    func_name: &str,
    runtime_fn: RuntimeFunction,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    ctx: &CompilerContext,
    module: &mut walrus::Module,
) -> Result<(), anyhow::Error> {
    if let Some(target) = &call_expr.target {
        // Method style: receiver.fn()
        if !call_expr.args.is_empty() {
            anyhow::bail!("{}() method expects 0 arguments", func_name);
        }
        compile_expr(&target.expr, body, env, ctx, module)?;
    } else {
        // Function style: fn(receiver)
        if call_expr.args.len() != 1 {
            anyhow::bail!("{}() function expects 1 argument", func_name);
        }
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
    }
    body.call(env.get(runtime_fn));
    Ok(())
}

/// Compile a method/function call that takes a receiver plus two arguments.
///
/// - Method style:   `receiver.fn(arg1, arg2)`  — `target` is `Some`, `args` has 2 elements
/// - Function style: `fn(receiver, arg1, arg2)` — `target` is `None`, `args` has 3 elements
///
/// Compiles all three expressions in order, then emits `call runtime_fn`.
pub fn compile_call_ternary(
    call_expr: &CallExpr,
    func_name: &str,
    runtime_fn: RuntimeFunction,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    ctx: &CompilerContext,
    module: &mut walrus::Module,
) -> Result<(), anyhow::Error> {
    if let Some(target) = &call_expr.target {
        // Method style: receiver.fn(arg1, arg2)
        if call_expr.args.len() != 2 {
            anyhow::bail!("{}() method expects 2 arguments", func_name);
        }
        compile_expr(&target.expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[1].expr, body, env, ctx, module)?;
    } else {
        // Function style: fn(receiver, arg1, arg2)
        if call_expr.args.len() != 3 {
            anyhow::bail!("{}() function expects 3 arguments", func_name);
        }
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[1].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[2].expr, body, env, ctx, module)?;
    }
    body.call(env.get(runtime_fn));
    Ok(())
}

/// Compile a method/function call with receiver plus three arguments (quaternary).
///
/// - Method style: `receiver.fn(a, b, c)` — target is Some, args has 3 elements
/// - Function style: `fn(receiver, a, b, c)` — target is None, args has 4 elements
pub fn compile_call_quaternary(
    call_expr: &CallExpr,
    func_name: &str,
    runtime_fn: RuntimeFunction,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    ctx: &CompilerContext,
    module: &mut walrus::Module,
) -> Result<(), anyhow::Error> {
    if let Some(target) = &call_expr.target {
        if call_expr.args.len() != 3 {
            anyhow::bail!("{}() method expects 3 arguments", func_name);
        }
        compile_expr(&target.expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[1].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[2].expr, body, env, ctx, module)?;
    } else {
        if call_expr.args.len() != 4 {
            anyhow::bail!("{}() function expects 4 arguments", func_name);
        }
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[1].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[2].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[3].expr, body, env, ctx, module)?;
    }
    body.call(env.get(runtime_fn));
    Ok(())
}

/// Compile a method/function call that takes a receiver plus one argument.
///
/// - Method style:   `receiver.fn(arg)`  — `target` is `Some`, `args` has 1 element
/// - Function style: `fn(receiver, arg)` — `target` is `None`, `args` has 2 elements
///
/// Compiles both expressions in order, then emits `call runtime_fn`.
pub fn compile_call_binary(
    call_expr: &CallExpr,
    func_name: &str,
    runtime_fn: RuntimeFunction,
    body: &mut InstrSeqBuilder,
    env: &CompilerEnv,
    ctx: &CompilerContext,
    module: &mut walrus::Module,
) -> Result<(), anyhow::Error> {
    if let Some(target) = &call_expr.target {
        // Method style: receiver.fn(arg)
        if call_expr.args.len() != 1 {
            anyhow::bail!("{}() method expects 1 argument", func_name);
        }
        compile_expr(&target.expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
    } else {
        // Function style: fn(receiver, arg)
        if call_expr.args.len() != 2 {
            anyhow::bail!("{}() function expects 2 arguments", func_name);
        }
        compile_expr(&call_expr.args[0].expr, body, env, ctx, module)?;
        compile_expr(&call_expr.args[1].expr, body, env, ctx, module)?;
    }
    body.call(env.get(runtime_fn));
    Ok(())
}