1 pontos por GN⁺ 2024-09-27 | 1 comentários | Compartilhar no WhatsApp
  • O High C Compiler para FM TOWNS, de 1989, ia além do suporte ao ambiente DOS e trazia vários recursos de linguagem voltados ao usuário que eram raros em compiladores C da época
  • Combinado ao DOS extender da Phar Lap, ele se tornou o compilador C 1st-party do FM TOWNS em um fluxo de desenvolvimento que aproveitava o 80386 de 32 bits no ambiente MS-DOS de 16 bits
  • Underscores em literais numéricos, argumentos nomeados, intervalos em case, funções aninhadas e geradores são recursos que só entraram nos padrões C/C++ muito mais tarde, ou que ainda hoje não existem no padrão
  • Funções aninhadas ofereciam “full function value” na forma de closures não escapáveis, passando juntas a ponteiro de função e ponteiro de contexto, o que era mais expressivo que ponteiros de função C comuns
  • Geradores eram implementados como açúcar sintático sobre funções aninhadas: o corpo do loop for do chamador era transformado em uma função aninhada e passado como argumento de yield, numa estrutura simples

O lugar do FM TOWNS e do High C

  • Um manual de compilador C dos anos 1980, encontrado em uma pilha de livros sobre FM TOWNS, continha extensões de linguagem mais ricas do que se esperava
  • Para usar C e linguagens da sua família em ambientes reais, por muito tempo foram necessárias extensões de fornecedores
    • Hoje, em ambientes centrados em GCC, Clang e MSVC, as extensões tendem a se concentrar em tratamento específico de plataforma ou controle de detalhes de baixo nível
    • Nos anos 1980, empresas menores e mais numerosas competiam por adoção, e as extensões também eram mais variadas
  • A Phar Lap criou um dos primeiros DOS extenders, que permitia usar o processador 80386 de 32 bits em ambientes MS-DOS de 16 bits
  • A MetaWare portou o High C Compiler para o SDK do DOS extender da Phar Lap a pedido da Phar Lap
  • A Fujitsu integrou o DOS extender da Phar Lap ao sistema operacional da plataforma FM TOWNS, baseada no 80386, e o High C se tornou o compilador C 1st-party dessa plataforma
  • O FM TOWNS foi lançado em 1989, pouco antes da ratificação do C89, o primeiro padrão ANSI C

Pequenas conveniências que vieram antes do padrão

  • Separadores underscore em literais numéricos

    • Era possível inserir separadores underscore dentro de números para tornar literais numéricos longos mais legíveis
    • C++ introduziu separadores com apóstrofo, como 1'000'000, no C++14
    • C só introduziu um recurso semelhante no C23
  • Argumentos nomeados

    • Era possível nomear argumentos em funções com muitos parâmetros ou que usam muitos tipos cujo significado não fica claro no ponto de chamada, como bool
    • Os argumentos nomeados do High C funcionam de modo parecido com o popular recurso do Python
      • O rótulo do argumento é opcional
      • Quando há rótulo, a sintaxe argumentName => value permite especificar argumentos em ordem arbitrária
      • Argumentos sem rótulo e com rótulo podem ser misturados, mas todos os parâmetros da função precisam ter um argumento correspondente
    • C e C++ padrão ainda não têm esse recurso
  • Intervalos em case

    • Oferecia um recurso para fazer match de um intervalo de valores de uma vez, como case low..high em Pascal
    • C e C++ padrão não adotaram esse recurso

Funções aninhadas e full function value

  • High C permite declarar funções aninhadas dentro de funções, como em Pascal
  • A forma de implementação se aproxima de uma versão mais completa do que as funções aninhadas do Pascal padrão ou da extensão de funções aninhadas do GCC
  • High C permite declarar não só funções aninhadas, mas também tipos full function value
    • Diferentemente dos ponteiros de função C tradicionais, eles guardam junto um ponteiro de função e um ponteiro de contexto
    • Isso permite reencontrar o contexto capturado por uma função aninhada
    • São closures não escapáveis, cuja vida útil não continua depois que a função externa retorna
  • A extensão de funções aninhadas do GCC escrevia código executável na pilha de chamadas para fazer thunk do ponteiro de contexto, tentando permitir que funções aninhadas fossem referenciadas como ponteiros de função comuns
    • Essa abordagem trouxe riscos sérios de segurança e levou muitas plataformas a desativarem completamente o recurso
  • Referências a funções locais no High C podem ser usadas como valores de primeira classe, mas sua vida útil não se estende para depois do retorno da função externa
  • Funções aninhadas também podem fazer goto para a função pai
    • Como blocos do Smalltalk, isso permite saídas não locais para fora da função aninhada
    • Com isso, é possível criar funções que se comportam como fluxo de controle
  • Objective-C ganhou blocks que podem ser usados como escaping closures em 2009, e C++ introduziu lambdas em 2011
  • Nenhum dos dois recursos tinha capacidade de saída não local
  • C padrão ainda não tem um recurso oficial de funções aninhadas

Corrotinas geradoras

  • A MetaWare destacava o recurso de geradores a ponto de dedicar um capítulo inteiro a ele
  • Em 1989, High C oferecia corrotinas geradoras ao estilo Python em plain C
  • Uma função geradora é declarada com a sintaxe void foo(Arg arguments) -> (Yield yields)
    • Dentro da função, pode-se chamar várias vezes a função mágica yield(values...) para produzir uma sequência de valores
    • O chamador percorre os valores gerados em sequência com uma nova sintaxe de loop for, na forma for variable... <- foo(arguments...) do { ... }
  • Essa implementação pode ser combinada de maneiras complexas com funções aninhadas
    • Uma função aninhada dentro de um gerador pode capturar a operação de yield do gerador externo
    • A função aninhada pode se chamar recursivamente para percorrer uma árvore ou estrutura de dados recursiva, dando yield em cada etapa
  • Esse formato parece difícil de implementar em Python ou em muitas linguagens mainstream com corrotinas geradoras

Como os geradores são implementados e a diferença em relação às linguagens padrão

  • Os geradores do High C funcionam como açúcar sintático sobre funções aninhadas, sem um runtime avançado
  • Uma declaração de gerador da forma void foo(Arg arguments) -> (Yield yields) equivale à declaração de função comum void foo(void yield(Yield yields)!, Arg arguments)
    • yield é um parâmetro implícito do tipo “full function value”
    • Uma chamada yield(values) dentro do corpo do gerador é uma chamada de função comum a esse parâmetro implícito de função
  • O corpo do loop for no lado do chamador é transformado em uma função aninhada
    • Essa função aninhada é passada como argumento yield do gerador
    • A estrutura é simples, mas efetiva
  • Como funções aninhadas dão suporte a saídas não locais, break, continue e goto que saem do corpo do loop for também funcionam por meio de goto para a posição adequada fora do loop
  • É improvável que C padrão tente integrar recursos desse tipo
  • C++20 oferece um recurso de corrotinas muito flexível e complexo, baseado em transformação de corrotinas em tempo de compilação
    • Parece possível usá-lo para implementar geradores
    • No entanto, o resultado provavelmente não se combinaria com funções locais de forma tão intuitiva

1 comentários

 
GN⁺ 2024-09-27
Comentários do Hacker News
  • Em 2011, eu havia documentado o for baseado em iteradores. Já era um daqueles recursos esquecidos havia muito tempo, e eu também tratei de como ele poderia ser caso entrasse no padrão do C++
    Por sorte, tenho um exemplar da versão em inglês do High C/C++ Language Reference
    http://jdebp.uk./FGA/metaware-iterator-driven-for.html
    http://jdebp.uk./Proposals/metaware-iterator-driven-for.html
    • Fico curioso sobre como break ou return eram compilados. Será que isso era transformado de modo que a função yield retornasse um código de estado e ele fosse verificado no ponto da chamada?
    • Aquela carinha sorridente invertida foi intencional?
  • D tem esses recursos, inclusive no Das BetterC: underscores em literais numéricos, intervalos em case, argumentos nomeados, funções aninhadas, funções aninhadas estáticas e um recurso parecido com geradores
    Por exemplo, dá para escrever int a = 1_234_567;, case 5 .. case 6:, test(b:3, a:4);
    Funções aninhadas estáticas não podem acessar variáveis do frame da função externa, então aparece um erro como Error: static function test.foo.plus cannot access variable i in frame of function test.foo
    O recurso parecido com geradores está em https://dlang.org/spec/statement.html#foreach_over_struct_an...
    • Passei o texto todo pensando em D. Fiquei achando que o Walter Bright apareceria nos comentários
    • Também considero o coletor de lixo do D um recurso realmente bom. Em código de baixo nível, às vezes é preciso gerenciamento manual de memória, mas na prática isso muitas vezes não importa, e há muitos casos em que o coletor de lixo torna as coisas bem mais fáceis
      Por exemplo, se você fizer um serviço de cache em memória, talvez seja melhor que os próprios itens do cache não sejam rastreados pelo coletor de lixo. Como ele muitas vezes não conhece o padrão real de acesso, pode acabar atrapalhando. Mas, para a maior parte dos outros componentes desse serviço, ter um coletor de lixo combina mais
    • Tenho uma pergunta. Alguém sabe por que as pessoas não gostam do conceito de funções aninhadas em C?
      E por que os argumentos nomeados são na forma test(a:4, b:3) e não test(.a=4, b.=3);?
      Também fico curioso sobre como seria possível lidar com tipos de primeira classe em C
  • Em relação a isso, o compilador C lcc-win adicionou sobrecarga de operadores, argumentos padrão de função e sobrecarga de função. A documentação diz para ver “generic functions” [1]
    O compilador C do Plan 9 também introduziu várias extensões de linguagem, e algumas delas, como structs/unions anônimas, depois entraram no padrão C. Hoje o GCC aceita a flag -fplan9-extensions [2], que pode habilitar recursos bem úteis, como a conversão automática de ponteiros para struct em campos anônimos em chamadas de função e atribuições
    [1] https://lcc-win32.services.net/C-Tutorial.pdf
    [2] https://gcc.gnu.org/onlinedocs/gcc/Unnamed-Fields.html
  • Quem será que foi o gênio que criou esses recursos? Parece que havia alguém com uma visão muito à frente do tempo naquela empresa
    É uma pena que isso não tenha se espalhado mais pelo mundo e influenciado os padrões da linguagem. Impressiona que esses recursos já existissem há tanto tempo
    Isso já apareceu antes no Hacker News: https://news.ycombinator.com/item?id=38938402
    Será que existe uma cópia em PDF em algum lugar?
    • O CLU já tinha, em meados para o fim dos anos 1970, um laço for com iteradores, ou seja, geradores e yield [0]. A linguagem Icon, de época parecida, também tinha um recurso de geradores semelhante [1], e usava suspend em vez de yield. Pelo que sei, Ada (1983) também tinha algo assim
      Esses recursos de linguagem não eram completamente desconhecidos
      [0] https://publications.csail.mit.edu/lcs/pubs/pdf/MIT-LCS-TR-2...
      [1] https://dl.acm.org/doi/pdf/10.1145/800055.802034
    • Há uma cópia do manual de referência do HC 1.2 (1985) no Bitsavers
      Ele explica underscores em números, intervalos em case, parâmetros nomeados, funções aninhadas e até variáveis de função completas
      https://bitsavers.org/pdf/metaware/…
      Basta olhar o Apêndice A, umas 50 páginas antes do fim do arquivo
    • A MetaWare era uma empresa prolífica de compiladores em Santa Cruz nos anos 80 e 90. Eu gostava do que eles faziam, e a cultura deles também era bem interessante
      Na época em que eu estava aprendendo e escrevendo código, acabei conhecendo isso por meio de sites meio suspeitos
    • Não é tão surpreendente assim. Se você mergulhar nos arquivos de linguagens de programação de alto nível posteriores a FORTRAN, Lisp, ALGOL e COBOL, vai ver muitas ideias de linguagem desse tipo
      Também vai descobrir a rica história das linguagens de programação de sistemas. E perceber como o design de C e de Go se parece no quanto ignorou o que estava acontecendo em outros ecossistemas e as experiências do passado
    • É uma pena que esses recursos pareçam novidades em vez de fazer parte da lista padrão de funcionalidades que a maioria das linguagens de programação oferece

O link para o manual do compilador está em https://winworldpc.com/product/metaware-high-c-cpp/33x
O PDF do manual de C traz uma indicação de copyright de 2007

  • O envio anterior e os comentários estão aqui: https://news.ycombinator.com/item?id=38938402
  • Se você estiver curioso sobre por que o literal de string nos exemplos da imagem termina com ¥n em vez de \n, esses exemplos de código parecem ter sido escritos em Shift-JIS. Em Shift-JIS, ¥ ocupa a posição do \ em ASCII
    • Originalmente era o JIS Roman [0], uma variação japonesa do ASCII de 1969. O Shift-JIS veio bem depois, adicionando suporte a conjuntos de caracteres de dois bytes
      [0] https://en.wikipedia.org/wiki/JIS_X_0201
    • O problema é que, em Shift-JIS, o código ASCII da barra invertida também é usado como o segundo byte de um caractere de 2 bytes. Então há casos em que literais de string em japonês não funcionam direito em C
      EUC-JP é melhor para esse uso, porque não tem esse problema. Em Pascal, se você usa comentários (* *) e não comentários { }, também não há esse problema ao usar Shift-JIS
    • O autor não deu informação sobre quando este livro foi publicado, e também não achei nada procurando. Mas parece provável que, quando o livro saiu, o padrão Shift-JIS ainda não existisse
      Em vez disso, é bem provável que estivesse sendo usado o JIS X 0201, que serviu de base para o Shift-JIS (https://en.m.wikipedia.org.org/wiki/JIS_X_0201)
    • De forma parecida, o prompt do DOS em japonês mostrava C:¥ em vez de C:\
  • Essas extensões são recursos de Ada. Ada tem rótulos no formato Call (Param_A => 1, Param_B => "Foo");, sublinhados em números de bases arbitrárias (X : Integer := 1_000;), subprogramas aninhados e verificação baseada em intervalos
    • Como o texto diz, Pascal já tinha esses recursos antes de Ada, e tipos de tarefa com pontos de entrada podem ser vistos, na prática, como geradores
      Parece que a gente frequentemente esquece o quanto C era incrivelmente primitivo na época em comparação com várias outras linguagens
  • Independentemente do conteúdo, a tipografia deste livro é interessante. Ao mesmo tempo bonita e terrível
    Não conheço o suficiente sobre convenções de escrita japonesa ou regras de kerning, mas parece que pegaram uma fonte proporcional com kanji e caracteres latinos e a forçaram a caber em células de largura fixa
    De todo modo, é bom que os exemplos de código não estejam em fonte de 8pt como em muitos livros que tenho
  • Os geradores me lembram a questão de iteração interna/externa em Rust e try_fold() (https://scribe.rip/@veedrac/rust-is-slow-and-i-am-the-cure-3...)
  • Especialmente os geradores parecem muito à frente do seu tempo. Talvez a Fujitsu simplesmente tenha conseguido implementá-los porque não se preocupava com um longo processo de padronização
    Mas, justamente por isso, parece que essas extensões ficaram relativamente desconhecidas e precisaram ser redescobertas e reinventadas décadas depois em C/C++ moderno
    • Não foi a Fujitsu, foi a MetaWare. A MetaWare era uma empresa com bastante experiência em compiladores e também tinha um compilador de Pascal relativamente conhecido na mesma época. Pascal já tinha funções aninhadas
    • C poderia ter sido uma linguagem muito melhor se não tivesse sido dominada por gente que insistia que nem complemento de dois devia entrar no padrão
    • Corrotinas e geradores já eram conceitos bem compreendidos naquela época. Basta olhar para Icon. Então realmente parece que a principal razão foi não precisar se preocupar com o ônus da padronização