1 puntos por GN⁺ 2024-09-27 | 1 comentarios | Compartir por WhatsApp
  • El High C Compiler para FM TOWNS de 1989 no solo era compatible con entornos DOS, sino que incluía varias funciones de lenguaje orientadas al usuario poco comunes en los compiladores C de la época
  • Al combinarse con el DOS extender de Phar Lap, se convirtió en el compilador C de primera parte de FM TOWNS dentro de un flujo de desarrollo que aprovechaba el 80386 de 32 bits en el entorno MS-DOS de 16 bits
  • Funciones como guiones bajos en literales numéricos, argumentos etiquetados, rangos en case, funciones anidadas y generadores llegaron a los estándares C/C++ mucho más tarde o todavía no forman parte del estándar
  • Las funciones anidadas ofrecían un “full function value” en forma de closure no escapante, que pasaba juntos un puntero a función y un puntero de contexto, con mayor expresividad que un puntero a función de C común
  • Los generadores se implementaban como azúcar sintáctico sobre funciones anidadas: convertían el cuerpo del bucle for del llamador en una función anidada y la pasaban como argumento de yield, con una estructura simple

El lugar de FM TOWNS y High C

  • Un manual de compilador C de los años 80, encontrado entre una pila de libros relacionados con FM TOWNS, contenía extensiones de lenguaje más ricas de lo esperado
  • Durante mucho tiempo, para usar C y sus lenguajes derivados en entornos reales hicieron falta extensiones de proveedor
    • Hoy, en entornos centrados en GCC, Clang y MSVC, las extensiones tienden a concentrarse en el manejo específico por plataforma o en el control de detalles de bajo nivel
    • En los años 80, con muchas empresas más pequeñas compitiendo por adopción, las extensiones también eran más variadas
  • Phar Lap creó uno de los primeros DOS extender, que permitía aprovechar procesadores 80386 de 32 bits en el entorno MS-DOS de 16 bits
  • MetaWare portó High C Compiler al SDK del DOS extender de Phar Lap por encargo de Phar Lap
  • Fujitsu integró el DOS extender de Phar Lap en el sistema operativo de la plataforma FM TOWNS basada en 80386, y High C se convirtió en el compilador C de primera parte de esta plataforma
  • FM TOWNS se lanzó en 1989, justo antes de que se ratificara C89, el primer estándar ANSI C

Pequeñas comodidades que se adelantaron al estándar

  • Separadores con guion bajo en literales numéricos

    • Para hacer más legibles los literales numéricos largos, se podían insertar separadores con guion bajo dentro de los números
    • C++ introdujo en C++14 separadores con apóstrofo como 1'000'000
    • C recién introdujo una función similar en C23
  • Argumentos etiquetados

    • Permitía poner nombres a los argumentos en funciones con muchos parámetros o que usaban muchos tipos como bool, cuyo significado no es fácil de ver en el punto de llamada
    • Los argumentos etiquetados de High C funcionaban de forma parecida a una función popular de Python
      • Las etiquetas de argumentos eran opcionales
      • Si había etiquetas, se podía especificar argumentos en cualquier orden con la sintaxis argumentName => value
      • Se podían mezclar argumentos sin etiqueta y con etiqueta, pero todos los parámetros de la función debían tener un argumento correspondiente
    • C y C++ estándar todavía no tienen esta función
  • Rangos en case

    • Ofrecía una función para hacer match con un rango de valores de una sola vez, como case low..high en Pascal
    • C y C++ estándar no adoptaron esta función

Funciones anidadas y full function value

  • High C permite declarar funciones anidadas dentro de una función, como Pascal
  • Su implementación se acerca a una forma más completa que las funciones anidadas del Pascal estándar o que la extensión de funciones anidadas de GCC
  • High C no solo permite declarar funciones anidadas, sino también declarar tipos full function value
    • A diferencia de los punteros a función tradicionales de C, guardan junto al puntero a función un puntero de contexto
    • Permiten recuperar el contexto capturado por una función anidada
    • Son closures no escapantes, cuya vida no continúa después de que retorna la función externa
  • La extensión de funciones anidadas de GCC, para permitir referenciar una función anidada como un puntero a función común, escribía código ejecutable en la pila de llamadas para hacer un thunk del puntero de contexto
    • Este enfoque generó un riesgo importante de seguridad y llevó a que muchas plataformas desactivaran por completo esa función
  • Las referencias a funciones locales en High C pueden usarse como valores de primera clase, pero su vida no se extiende más allá del retorno de la función externa
  • Una función anidada también puede hacer goto hacia la función padre
    • Como los bloques de Smalltalk, permite una salida no local que abandona la función anidada
    • Con esto se pueden crear funciones que se comportan como flujo de control
  • Objective-C obtuvo en 2009 blocks que pueden usarse como closures escapantes, y C++ introdujo lambdas en 2011
  • Ninguna de las dos funciones tenía capacidad de salida no local
  • C estándar todavía no tiene una función oficial de funciones anidadas

Coroutines generadoras

  • MetaWare destacaba tanto la función de generadores que le dedicó un capítulo entero
  • En 1989, High C admitía coroutines generadoras al estilo Python en plain C
  • Una función generadora se declara con la sintaxis void foo(Arg arguments) -> (Yield yields)
    • Dentro de la función se puede llamar varias veces a la función mágica yield(values...) para generar una secuencia de valores
    • El llamador recorre en orden los valores generados con la nueva sintaxis de bucle for, con la forma for variable... <- foo(arguments...) do { ... }
  • Esta implementación podía combinarse de maneras complejas con funciones anidadas
    • Una función anidada dentro de un generador podía capturar la acción yield del generador externo
    • Una función anidada podía llamarse recursivamente a sí misma para recorrer un árbol o una estructura de datos recursiva y hacer yield en cada paso
  • Esta forma parece difícil de implementar en Python o en muchos lenguajes populares con coroutines generadoras

Modo de implementación de los generadores y diferencias con lenguajes estándar

  • Los generadores de High C funcionan como azúcar sintáctico sobre funciones anidadas, sin un runtime avanzado
  • Una declaración de generador con la forma void foo(Arg arguments) -> (Yield yields) equivale a una declaración de función común void foo(void yield(Yield yields)!, Arg arguments)
    • yield es un parámetro implícito de tipo “full function value”
    • Una llamada yield(values) dentro del cuerpo del generador es una llamada común a ese parámetro de función implícito
  • El cuerpo del bucle for del lado del llamador se transforma en una función anidada
    • Esa función anidada se pasa como argumento yield del generador
    • La estructura es simple, pero efectiva
  • Como las funciones anidadas admiten salida no local, break, continue y goto que salen del cuerpo del bucle for también funcionan haciendo goto a la ubicación apropiada fuera del bucle
  • Es poco probable que C estándar intente integrar una función así
  • C++20 ofrece una función de coroutines muy flexible y compleja, basada en transformación de coroutines en tiempo de compilación
    • Parece posible usarla para implementar generadores
    • Sin embargo, el resultado probablemente no se combinaría de forma tan intuitiva con funciones locales

1 comentarios

 
GN⁺ 2024-09-27
Opiniones de Hacker News
  • En 2011 había documentado el for basado en iteradores. Ya era una de esas funciones olvidadas desde hacía mucho tiempo, y también traté cómo se vería si entrara en el estándar de C++ de la época
    Por suerte tengo un ejemplar de la versión en inglés de High C/C++ Language Reference
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • Me da curiosidad cómo se compilaban break o return. ¿Tendría que haber sido una transformación donde la función yield devolviera un código de estado y este se verificara en el punto de llamada?
    • ¿Eso era intencionalmente una carita sonriente invertida?
  • D, incluso Das BetterC, tiene estas funciones: guiones bajos en literales numéricos, rangos en case, argumentos con nombre, funciones anidadas, funciones anidadas estáticas y algo parecido a generadores
    Por ejemplo, se pueden usar formas como int a = 1_234_567;, case 5 .. case 6:, test(b:3, a:4);
    Las funciones anidadas estáticas no pueden acceder a las variables del frame de la función externa, por lo que dan errores como Error: static function test.foo.plus cannot access variable i in frame of function test.foo
    Lo parecido a generadores está en https://dlang.org/spec/statement.html#foreach_over_struct_an...
    • Durante toda la lectura de este artículo pensé en D. Me imagino que Walter Bright aparecerá en los comentarios
    • También creo que el recolector de basura de D es una función realmente buena. En código de bajo nivel hay momentos en que se necesita gestión manual de memoria, pero en la práctica muchas veces no importa demasiado, y hay muchas partes donde un recolector de basura facilita mucho las cosas
      Por ejemplo, si haces un servicio de caché en memoria, conviene que el recolector de basura no rastree los elementos de caché en sí, porque a menudo no conoce los patrones reales de acceso y puede estorbar. Pero para la mayoría de los demás componentes de ese servicio encaja mejor tener recolector de basura
    • Tengo una pregunta. ¿Alguien sabe por qué a la gente no le gusta el concepto de funciones anidadas en C?
      ¿Por qué los argumentos con nombre son de la forma test(a:4, b:3) y no test(.a=4, b.=3);?
      También me intriga cómo se podrían manejar tipos de primera clase en C
  • Relacionado con esto, el compilador C lcc-win agregó sobrecarga de operadores, argumentos de función por defecto y sobrecarga de funciones. En la documentación hay que ver “generic functions” [1]
    El compilador C de Plan 9 también introdujo varias extensiones del lenguaje, y algunas de ellas, como estructuras/uniones anónimas, luego entraron en el estándar de C. Actualmente GCC acepta la bandera -fplan9-extensions [2], que permite activar funciones bastante útiles, como convertir automáticamente punteros a estructuras en campos anónimos en llamadas a funciones y asignaciones
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • ¿Quién habrá sido el genio que creó estas funciones? Parece que dentro de esa empresa había alguien con mucha visión de futuro
    Es una lástima que no se hayan difundido ampliamente ni influido en los estándares del lenguaje. Sorprende que existieran funciones así hace tanto tiempo
    Ya se había hablado de esto antes en Hacker News: https://news.ycombinator.com/item?id=38938402
    ¿Habrá alguna copia en PDF en algún lado?
    • CLU ya tenía a mediados y fines de los años 70 iteradores, es decir, un bucle for con generadores y yield [0]. El lenguaje Icon, de una época similar, también tenía una función de generadores parecida [1], y usaba suspend para yield. Hasta donde sé, Ada (1983) también tenía algo así
      Estas funciones de lenguaje no eran completamente desconocidas
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • En Bitsavers hay una copia del manual de referencia de HC 1.2 (1985)
      Describe guiones bajos dentro de números, rangos en case, parámetros con nombre, funciones anidadas e incluso variables de función completas
      https://bitsavers.org/pdf/metaware/…
      Hay que ver el Appendix A, unas 50 páginas antes del final del archivo
    • MetaWare fue una prolífica empresa de compiladores en Santa Cruz durante los años 80 y 90. Me gustaba lo que hacían, y su cultura también era bastante interesante
      En mis tiempos de aprender y usar código, la conocí a través de sitios un poco sospechosos
    • No es tan sorprendente. Si uno se mete en los archivos de lenguajes de programación de alto nivel posteriores a FORTRAN, Lisp, ALGOL y COBOL, aparecen muchas ideas de lenguaje como estas
      También se descubre la rica historia de los lenguajes de programación de sistemas. Y se ve hasta qué punto los diseños de C y Go se parecen en haber ignorado lo que ocurría en otros ecosistemas y las experiencias del pasado
    • Es una lástima que estas funciones parezcan funciones nuevas en vez de formar parte de la lista estándar de funciones que ofrecen la mayoría de los lenguajes de programación

El enlace al manual del compilador está en https://winworldpc.com/product/metaware-high-c-cpp/33x
El PDF del manual de C muestra copyright de 2007

  • La publicación anterior y los comentarios están aquí: https://news.ycombinator.com/item?id=38938402
  • Si te preguntas por qué los literales de cadena en los ejemplos de la imagen terminan en ¥n en vez de \n, parece que estos ejemplos de código fueron escritos en Shift-JIS. En Shift-JIS, ¥ ocupa la posición del \ de ASCII
    • Originalmente era JIS Roman [0], una variante japonesa de ASCII de 1969. Shift-JIS agregó mucho después soporte para conjuntos de caracteres de doble byte
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • El problema es que en Shift-JIS el código ASCII de la barra invertida también se usa como segundo byte de caracteres de 2 bytes. Por eso a veces los literales de cadena en japonés no funcionan correctamente en C
      Para este uso, EUC-JP es mejor, porque no tiene ese problema. En Pascal, si usas comentarios (* *) y no comentarios { }, con Shift-JIS no tienes este problema
    • El autor no da información sobre cuándo salió este libro, y no se encuentra información al buscar. Pero parece probable que, cuando se publicó, el estándar Shift-JIS todavía no existiera
      En su lugar, es muy probable que se usara JIS X 0201(https://en.m.wikipedia.org.org/wiki/JIS_X_0201), que fue la base de Shift-JIS
    • De forma similar, el prompt de DOS en japonés era C:¥, no C:\
  • Estas extensiones son funciones de Ada. Ada tiene etiquetas con la forma Call (Param_A => 1, Param_B => "Foo");, guiones bajos en números de base arbitraria (X : Integer := 1_000;), subprogramas anidados y comprobaciones basadas en rangos
    • Como dice el artículo, Pascal ya tenía estas funciones desde antes que Ada, y los tipos de tarea con puntos de entrada pueden verse, en la práctica, como generadores
      Creo que a menudo olvidamos lo increíblemente primitivo que era C en comparación con varios otros lenguajes de la época
  • Al margen del contenido, la tipografía de este libro es interesante. Da una sensación hermosa y horrible al mismo tiempo
    No conozco lo suficiente la notación japonesa ni las reglas de kerning, pero parece como si hubieran tomado una fuente de ancho variable que incluye tanto kanji como caracteres latinos y la hubieran metido a la fuerza en celdas de ancho fijo
    En cualquier caso, me gusta que los ejemplos de código no estén en una fuente de 8 pt como en muchos de los libros que tengo
  • Al ver los generadores me viene a la mente el problema de iteración interna/externa de Rust y try_fold() (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • En particular, al ver los generadores, parecen muy adelantados a su época. Quizá Fujitsu pudo simplemente implementarlos porque no tenía que preocuparse por un largo proceso de estandarización
    Pero justamente por esa razón parece que estas extensiones fueron relativamente poco conocidas, y décadas después tuvieron que redescubrirse y reinventarse en C/C++ moderno
    • No fue Fujitsu, fue MetaWare. MetaWare era una empresa con bastante experiencia en compiladores y, por la misma época, también tenía un compilador de Pascal bastante conocido. Pascal ya tenía funciones anidadas
    • C podría haber sido un lenguaje mucho mejor si no hubiera quedado dominado por gente que insistía en que ni siquiera el complemento a dos debía incluirse en el estándar
    • Las corrutinas y los generadores ya eran conceptos bien entendidos en ese entonces. Basta con ver Icon. Así que, en realidad, la razón principal parece más bien haber sido no tener que preocuparse por la carga de la estandarización