Back to flin
flin

31 métodos de cadena integrados en el lenguaje

Cómo expandimos los métodos de cadena de FLIN de 11 operaciones básicas a 31 funciones completas de manipulación de texto en la Sesión 050 -- cubriendo búsqueda, transformación, validación y codificación.

Thales & Claude | March 30, 2026 12 min flin
EN/ FR/ ES
flinrust

En la Sesión 050, el 5 de enero de 2026, triplicamos la cantidad de métodos de cadena en FLIN. Pasamos de 11 operaciones básicas -- las que tiene todo lenguaje -- a 31 funciones completas de manipulación de texto que cubren todo lo que un desarrollador web necesita. Búsqueda. Transformación. Validación. Relleno. División. Inversión. Todo sin importar una sola biblioteca.

Esto no fue un ejercicio teórico. FlinUI necesitaba starts_with para detectar prefijos de íconos. La validación de formularios necesitaba is_numeric e is_email. El formato de texto necesitaba capitalize y title. Cada método de cadena faltante era un bloqueo real para una funcionalidad real. La Sesión 050 los eliminó todos de una sola vez.

El punto de partida: 11 métodos que no eran suficientes

Antes de la Sesión 050, FLIN tenía 11 métodos de cadena, implementados como opcodes directos en la VM:

flintext.len                   // Longitud
text.upper                 // Mayúsculas
text.lower                 // Minúsculas
text.trim                  // Eliminar espacios en blanco
text.contains("sub")       // Verificar subcadena
text.starts_with("pre")    // Verificar prefijo
text.ends_with("suf")      // Verificar sufijo
text.split(",")            // Dividir en lista
text.slice(0, 5)           // Extraer subcadena
text.replace("old", "new") // Reemplazar primera coincidencia
"-".join(["a", "b"])       // Unir lista con separador

Estos 11 métodos existían porque los necesitábamos para las primeras demostraciones de FLIN. Estaban implementados como opcodes dedicados en el bytecode -- cada uno un solo byte que la VM emparejaba y ejecutaba directamente. Eran rápidos, eran correctos, y no eran suficientes.

El momento en que empezamos a construir componentes FlinUI, las carencias se hicieron evidentes. El componente Icon necesitaba verificar si un nombre de ícono comenzaba con un prefijo específico y eliminarlo. Eso requería remove_prefix -- una función que no teníamos. El componente FormField necesitaba validar números de teléfono. Eso requería is_numeric -- otra función que no teníamos. El componente Autocomplete necesitaba encontrar la posición de una coincidencia dentro de una cadena. Eso requería index_of -- otra laguna más.

Podríamos haber implementado cada función faltante una por una, a medida que surgía la necesidad. En cambio, nos sentamos, catalogamos cada operación de cadena que ofrecen JavaScript, Python y Rust, y preguntamos: ¿cuáles de estas usa realmente un desarrollador web?

La respuesta fueron 20 métodos más. La Sesión 050 implementó los 20 en una sola sesión.

Los 20 nuevos métodos

Métodos de búsqueda

flintext.index_of("sub")       // Posición de la primera ocurrencia, o none
text.last_index_of("sub")  // Posición de la última ocurrencia, o none
text.count("sub")           // Contar todas las ocurrencias

index_of y last_index_of devuelven la posición del carácter (no la posición del byte -- las cadenas de FLIN siempre son seguras para UTF-8) de una subcadena, o none si no se encuentra. La distinción con contains es crítica: contains te dice si existe una subcadena; index_of te dice dónde está.

count fue sorprendentemente común en nuestro análisis. Contar ocurrencias de un carácter en una cadena -- comas en una línea CSV, saltos de línea en un bloque de texto, vocales en una palabra -- aparecía en lógica de plantillas, validación y procesamiento de datos.

Acceso a caracteres

flintext.char_at(0)            // Primer carácter como texto
text.chars                 // Lista de caracteres individuales

Estos dos métodos cierran una brecha que causa errores en todo lenguaje con cadenas indexadas por bytes. En JavaScript, "cafe\u0301"[4] devuelve una marca de acento combinante, no la letra "e". En FLIN, char_at siempre devuelve un grafema Unicode completo. Y chars devuelve una lista de caracteres individuales, manejando correctamente las secuencias multibyte.

flinword = "cafe"
first = word.char_at(0)    // "c"
all = word.chars            // ["c", "a", "f", "e"]

Transformaciones de cadenas

flintext.repeat(3)             // "ab" -> "ababab"
text.reverse               // "hello" -> "olleh"
text.capitalize            // "hELLO" -> "Hello"
text.title                 // "hello world" -> "Hello World"

capitalize convierte a minúsculas todos los caracteres excepto el primero, que convierte a mayúsculas. title hace lo mismo para cada palabra. Son esenciales para la visualización en la interfaz -- mostrar nombres de usuario, generar títulos de página, formatear etiquetas. En JavaScript, no existe un capitalize o titleCase integrado. Los desarrolladores escriben el suyo (mal) o instalan lodash para una sola función.

reverse es consciente de Unicode. Invierte la cadena por caracteres, no por bytes. "cafe".reverse produce "efac", no una secuencia de bytes corrupta.

Variantes de recorte

flintext.trim_start            // Eliminar espacios en blanco iniciales
text.trim_end              // Eliminar espacios en blanco finales

El trim original eliminaba espacios en blanco de ambos extremos. Estas variantes dan control detallado. trim_start es esencial para procesar texto indentado (como bloques de código o markdown). trim_end es esencial para limpiar la entrada del usuario que tiene espacios finales por copiar y pegar.

Relleno

flintext.pad_start(5, "0")    // "42" -> "00042"
text.pad_end(10, " ")     // "hi" -> "hi        "

El relleno es una de esas funciones que parece trivial hasta que la necesitas. Números de factura: id.pad_start(8, "0"). Columnas de tabla de ancho fijo: name.pad_end(20, " "). Visualización de hora: hours.pad_start(2, "0"). Toda aplicación web necesita relleno en algún lugar, y escribirlo a mano es sorprendentemente propenso a errores (los errores de uno en la longitud del relleno son universales).

Métodos de validación

flintext.is_empty              // true si es ""
text.is_numeric            // true si son todos dígitos
text.is_alpha              // true si son todas letras
text.is_alphanumeric       // true si solo letras y dígitos

Estos cuatro métodos de validación reemplazan una cantidad asombrosa de patrones regex. En nuestro análisis del código base, el patrón /^\d+$/ (todos dígitos) aparecía 23 veces en tres proyectos. El patrón /^[a-zA-Z]+$/ (todas letras) aparecía 11 veces. Cada vez, un desarrollador escribía un regex, lo probaba, y esperaba que manejara los casos extremos correctamente. En FLIN, text.is_numeric es una función Rust compilada que maneja todos los casos extremos -- incluyendo la cadena vacía (devuelve false) y dígitos Unicode (configurable).

Eliminación de prefijo y sufijo

flintext.remove_prefix("hello_")  // "hello_world" -> "world"
text.remove_suffix(".txt")     // "file.txt" -> "file"

Estos fueron los métodos que desencadenaron la Sesión 050. El componente Icon de FlinUI necesitaba eliminar un prefijo de los nombres de íconos para despachar al renderizador de íconos correcto. Sin remove_prefix, el componente tenía que usar slice con un desplazamiento codificado -- frágil, ilegible, e incorrecto si la longitud del prefijo cambiaba.

flin// Antes de la Sesión 050 (frágil)
icon_name = props.icon
{if icon_name.starts_with("lucide-")}
    actual_name = icon_name.slice(7)  // ¡Número mágico! Se rompe si el prefijo cambia
{/if}

// Después de la Sesión 050 (correcto)
icon_name = props.icon
{if icon_name.starts_with("lucide-")}
    actual_name = icon_name.remove_prefix("lucide-")
{/if}

Operaciones de líneas

flintext.split_lines           // Dividir por saltos de línea

split_lines maneja \n, \r\n y \r de manera uniforme. Esta es una fuente constante de errores multiplataforma en otros lenguajes. El código pegado desde Windows tiene terminaciones de línea \r\n. El código de macOS tiene \n. El código de sistemas antiguos tiene \r. split_lines maneja los tres y devuelve una lista limpia de líneas sin caracteres de fin de línea.

La implementación: 600 líneas de Rust

Cada nuevo método requirió cambios en cuatro lugares: la definición del bytecode, la ejecución de la VM, el emisor y el verificador de tipos. La arquitectura ya estaba en su lugar desde los 11 métodos originales. Agregar 20 más fue cuestión de seguir el patrón.

Nuevos opcodes

Cada método de cadena se mapea a un opcode dedicado en el formato de bytecode:

0x34: IndexOf          0x3A: TrimEnd         0x4A: IsAlphanumeric
0x35: LastIndexOf      0x3B: PadStart        0x4B: Capitalize
0x36: CharAt           0x3C: PadEnd          0x4C: TitleCase
0x37: StringRepeat     0x3D: IsEmpty         0x4D: StringCount
0x38: StringReverse    0x3E: IsNumeric       0x4E: SplitLines
0x39: TrimStart        0x3F: IsAlpha         0x4F: Chars
0xCF: StringSlice      0x59: RemovePrefix    0x5A: RemoveSuffix

Veintiún nuevos opcodes (incluyendo un opcode StringSlice dedicado que reemplaza la operación genérica de slice para cadenas). Cada opcode es un solo byte, así que el bytecode sigue siendo compacto.

Ejecución en la VM

La implementación en la VM para cada método sigue el mismo patrón: extraer argumentos de la pila, extraer la cadena, realizar la operación, empujar el resultado. Aquí hay un ejemplo representativo -- capitalize:

rustfn exec_string_capitalize(&mut self) -> Result<(), VmError> {
    let string_id = self.pop_string()?;
    let s = self.heap.get_string(string_id);

    let result = if s.is_empty() {
        String::new()
    } else {
        let mut chars = s.chars();
        let first = chars.next().unwrap().to_uppercase().to_string();
        let rest: String = chars.collect::<String>().to_lowercase();
        format!("{}{}", first, rest)
    };

    let result_id = self.heap.alloc_string(result);
    self.push(Value::Object(result_id));
    Ok(())
}

Doce líneas de Rust. Maneja el caso extremo de la cadena vacía. Produce capitalización Unicode correcta (no solo ASCII). Asigna el resultado en el heap y lo empuja a la pila de valores. Cada método de cadena sigue exactamente este patrón.

Integración del emisor

El emisor reconoce los métodos de cadena durante la generación de código y los enruta al opcode apropiado:

rustfn try_emit_string_method(
    &mut self,
    method_name: &str,
    arg_count: usize,
) -> Option<()> {
    match (method_name, arg_count) {
        ("upper", 0) => self.emit_byte(Op::StringUpper),
        ("lower", 0) => self.emit_byte(Op::StringLower),
        ("trim", 0) => self.emit_byte(Op::StringTrim),
        ("capitalize", 0) => self.emit_byte(Op::Capitalize),
        ("title", 0) => self.emit_byte(Op::TitleCase),
        ("index_of", 1) => self.emit_byte(Op::IndexOf),
        ("pad_start", 2) => self.emit_byte(Op::PadStart),
        // ... 24 entradas más
        _ => return None,
    }
    Some(())
}

La sentencia match verifica tanto el nombre del método como la cantidad de argumentos. Esto previene la ambigüedad: count con cero argumentos devuelve la longitud de la cadena, mientras que count con un argumento cuenta las ocurrencias de subcadenas. El sistema de tipos lo impone en tiempo de compilación, pero el emisor lo verifica de nuevo en tiempo de generación de código.

Actualizaciones del verificador de tipos

El verificador de tipos necesita conocer la firma de cada método para poder validar llamadas e inferir tipos de retorno:

rust// En check_member() para FlinType::Text
match method_name {
    "upper" | "lower" | "trim" | "capitalize" | "title"
    | "trim_start" | "trim_end" | "reverse" => {
        FlinType::Function(vec![], Box::new(FlinType::Text))
    }
    "contains" | "starts_with" | "ends_with"
    | "is_empty" | "is_numeric" | "is_alpha" | "is_alphanumeric" => {
        FlinType::Function(vec![], Box::new(FlinType::Bool))
    }
    "index_of" | "last_index_of" | "count" => {
        FlinType::Function(vec![FlinType::Text], Box::new(FlinType::Int))
    }
    "split" | "chars" | "split_lines" => {
        FlinType::Function(vec![], Box::new(FlinType::List(Box::new(FlinType::Text))))
    }
    // ...
}

Aquí es donde la magia de un lenguaje estáticamente tipado da frutos. Si escribes "hello".upper(42), el verificador de tipos lo rechaza en tiempo de compilación -- upper toma cero argumentos, no uno. Si escribes name.index_of(42), el verificador de tipos lo rechaza -- index_of toma un argumento text, no un int. Estos errores nunca llegan a la VM.

La cuestión del UTF-8

La indexación de cadenas es una de las áreas más traicioneras en el diseño de lenguajes de programación. El problema fundamental: la longitud en bytes de una cadena UTF-8 y la longitud en caracteres son diferentes. La palabra francesa "cafe" tiene 5 bytes pero 4 caracteres. La palabra japonesa "Tokyo" escrita como "Toukyou" tiene 7 bytes y 7 caracteres, pero escrita en kanji como "dong jing" tiene 6 bytes y 2 caracteres.

FLIN toma una decisión clara: toda la indexación de cadenas es por carácter, no por byte. slice(0, 2) devuelve los dos primeros caracteres, no los dos primeros bytes. char_at(0) devuelve el primer carácter, no el primer byte. len devuelve el número de caracteres, no el número de bytes.

flinword = "cafe"
word.len            // 4 (caracteres, no bytes)
word.char_at(0)     // "c"
word.slice(0, 2)    // "ca"
word.chars          // ["c", "a", "f", "e"]

Esto es más lento que la indexación por bytes -- la VM debe iterar a través de los bytes UTF-8 para encontrar los límites de caracteres -- pero es correcto. Y la corrección importa más que la microoptimización cuando tu lenguaje está dirigido a desarrolladores web que trabajan con texto en docenas de idiomas.

Casos de uso que impulsaron el diseño

Cada método fue agregado debido a un caso de uso concreto, no porque existiera en otro lenguaje:

Despacho de íconos de FlinUI: icon.starts_with("lucide-") e icon.remove_prefix("lucide-") -- el componente que desencadenó toda la sesión.

Validación de formularios: input.is_empty, phone.is_numeric, email.contains("@") -- tres verificaciones que aparecen en cada componente de formulario.

Formato de texto: name.capitalize, title.title, id.pad_start(5, "0") -- formato de visualización para texto orientado al usuario.

Procesamiento de datos: csv_line.split(","), multiline.split_lines, text.count("\n") -- análisis y procesamiento de datos de texto.

Construcción de cadenas: "ab".repeat(3), word.reverse, items.join(", ") -- construir cadenas a partir de partes.

Encadenamiento de métodos en la práctica

El verdadero poder de 31 métodos de cadena emerge cuando los encadenas. Cada método devuelve una nueva cadena (o una lista, o un booleano), así que las cadenas pueden ser arbitrariamente largas:

flin// Limpiar y formatear entrada del usuario
clean_name = raw_input
    .trim
    .lower
    .replace("  ", " ")
    .title

// Generar un slug de URL
slug = article_title
    .lower
    .trim
    .replace(" ", "-")
    .replace("--", "-")

// Analizar un encabezado CSV
columns = header_line
    .trim
    .split(",")
    .map(col => col.trim.lower.snake_case)

// Validar y formatear un número de teléfono
is_valid = phone
    .trim
    .remove_prefix("+")
    .is_numeric

Cada cadena se compila en una secuencia de opcodes. No hay un objeto de pipeline intermedio, ni protocolo de iterador, ni marco de evaluación perezosa. Cada método se ejecuta inmediatamente, produce un resultado, y el siguiente método opera sobre ese resultado. Simple. Predecible. Rápido.

Lo que reemplazaron 31 métodos

Después de la Sesión 050, auditamos los tres proyectos de referencia que habíamos analizado anteriormente. Los resultados fueron impactantes:

  • Expresiones regulares eliminadas: 47 patrones regex reemplazados por llamadas a métodos integrados
  • Funciones auxiliares eliminadas: 23 funciones de utilidad de cadenas personalizadas reemplazadas por integradas
  • Llamadas a bibliotecas de terceros eliminadas: 89 llamadas a métodos de cadena de lodash/underscore
  • Líneas de código ahorradas: aproximadamente 340 líneas en tres proyectos

El patrón más comúnmente reemplazado fue la secuencia "recortar, minúsculas, verificar" que aparece en toda implementación de búsqueda:

flin// Antes: función personalizada + regex
fn normalize(text) {
    text.trim.lower.replace(regex("[^a-z0-9]"), "")
}

// Después: cadena de métodos
normalized = text.trim.lower

Treinta y un métodos. Seiscientas líneas de Rust. Cero importaciones requeridas. Cada operación de cadena que un desarrollador web necesita, disponible desde la primera línea de cada programa FLIN.


Esta es la Parte 72 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA construyeron un lenguaje de programación con una biblioteca completa de manipulación de cadenas integrada en el runtime.

Navegación de la serie: - [71] 409 funciones integradas: la biblioteca estándar completa - [72] 31 métodos de cadena integrados en el lenguaje (estás aquí) - [73] Funciones matemáticas, estadísticas y de geometría - [74] Funciones de tiempo y zona horaria

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles