Escreva um intérprete de turno

10

EDIT: Como alguns de vocês suspeitavam, houve um erro no intérprete oficial: a ordem da composição .foi invertida. Eu tinha duas versões do intérprete e usei a errada aqui. Os exemplos também foram escritos para esta versão incorreta. Corrigi o intérprete no repositório e os exemplos abaixo. A descrição de >também era um pouco ambígua, então eu corrigi isso. Além disso, desculpas por isso demorar tanto, eu fui pego em algumas coisas da vida real.

EDIT2: Meu intérprete teve um erro cuja implementação .foi refletida nos exemplos (eles contavam com um comportamento indefinido). O problema está resolvido.

Introdução

Shift é uma linguagem de programação funcional esotérica que criei há alguns anos, mas publiquei hoje. É baseado em pilha, mas também possui currying automático como Haskell.

Especificação

Existem dois tipos de dados no Shift:

  • Funções, que possuem uma aridade positiva arbitrária (número de entradas) e que retornam uma lista de saídas. Por exemplo, uma função que duplica sua única entrada possui aridade 1 e uma função que troca suas duas entradas tem aridade 2.
  • Espaços em branco, todos idênticos e não têm outro objetivo além de não serem funções.

Um programa Shift consiste em zero ou mais comandos , cada um com um único caractere ASCII. Existem 8 comandos no total:

  • !( aplicar ) exibe uma função fe um valor xda pilha e aplica f- se a x. Se ftiver arity 1, a lista f(x)será adicionada à frente da pilha. Se houver aridade n > 1, uma nova (n-1)função -ary gé enviada para a pilha. É preciso entradas e retornos .x1,x2,...,xn-1f(x,x1,x2,...,xn-1)
  • ?(em branco ) coloca um espaço em branco na pilha.
  • +( clone ) envia para a pilha uma função unária que duplica sua entrada: qualquer valor xé mapeado para [x,x].
  • >( shift ) empurra para a pilha uma função unária que assume uma nfunção -ary fe retorna uma (n+1)função -ary gque ignora seu primeiro argumento x, chama fos restantes e se alinha xà frente do resultado. Por exemplo, shift(clone)é uma função binária que recebe entradas a,be retornos [a,b,b].
  • /( fork ) empurra para a pilha uma função ternária que aceita três entradas a,b,ce retorna [b]se aestiver em branco ou [c]não.
  • $( Chamada ) empurra para a pilha uma função binária que aparece uma função fe um valor x, e aplica-se fa xexatamente como !faz.
  • .( cadeia ) empurra para a pilha uma função binária que exibe duas funções fe gretorna sua composição: uma função hque tem a mesma aridade fe que recebe suas entradas normalmente, aplica f- se a elas e depois aplica - se totalmenteg ao resultado (chamadas tantas vezes quanto dita a sua aridade), com itens não utilizados da saída do frestante no resultado de h. Por exemplo, suponha que fseja uma função binária que clone seu segundo argumento e gseja chamada . Se a pilha contém [f,g,a,b,c]e nós o fazemos .!!, então ela contém [chain(f,g),a,b,c]; se fizermos a !!seguir, fé aplicado primeiro a,b, produzindo[a,b,b], então gé aplicado aos dois primeiros elementos disso, já que sua aridade é 2, produzindo [a(b),b], e a pilha finalmente será [a(b),b,c].
  • @( digamos ) empurra uma função unária que simplesmente retorna sua entrada e imprime 0se estivesse em branco e 1se fosse uma função.

Observe que todos os comandos, exceto !simplesmente enviam um valor para a pilha, não há como executar entradas e a única maneira de produzir qualquer coisa é usar @. Um programa é interpretado avaliando os comandos um por um, imprimindo 0s ou 1s sempre que "say" é chamado e saindo. Qualquer comportamento não descrito aqui (aplicar um espaço em branco, aplicar uma pilha de comprimento 0 ou 1, chamar "cadeia" em um espaço em branco etc.) é indefinido: o intérprete pode travar, falhar silenciosamente, solicitar informações ou qualquer outra coisa.

A tarefa

Sua tarefa é escrever um intérprete para o Shift. Deve levar do STDIN, da linha de comando ou do argumento da função um programa Shift a ser interpretado e imprimir em STDOUT ou retornar a saída resultante (possivelmente infinita) de 0s e 1s. Se você escreve uma função, deve poder acessar as saídas de tamanho infinito de alguma forma (gerador em Python, lista lenta em Haskell, etc). Como alternativa, você pode pegar outra entrada, um número ne retornar pelo menos ncaracteres da saída, se for maior que n.

A menor contagem de bytes vence e as brechas padrão não são permitidas.

Casos de teste

Este programa Shift imprime 01:

?@!@@!

Começando pela esquerda: pressione um espaço em branco, pressione dizer e aplique a palavra no espaço em branco. Isso gera 0. Em seguida, pressione say duas vezes e aplique a segunda palavra à primeira. Isso gera 1.

Este programa faz um loop para sempre, não produzindo saída:

$+.!!+!!

Empurre a chamada e o clone e aplique a cadeia a eles (precisamos de dois !s, pois a cadeia é uma função binária). Agora a pilha contém uma função que pega um argumento, o duplica e chama a primeira cópia no segundo. Com +!!, duplicamos essa função e a chamamos por si mesma.

Este programa imprime 0010:

?@$.++>!.!!.!!.!!!!+?/!!!@!@>!!!

Empurre um espaço em branco e diga . Em seguida, componha uma função binária que copie seu segundo argumento b, copie o primeiro ae o componha consigo mesmo, depois aplique a composição à cópia de b, retornando [a(a(b)),b]. Aplique para dizer e em branco, depois diga para os dois elementos restantes na pilha.

Este programa é impresso 0. Para cada um !!!que você anexa, ele imprime um adicional 0.

?@+$>!>!+>!///!!>!>!.!!.!!.!!+!!!!

Empurre um espaço em branco e diga . Em seguida, componha uma função ternária que tome f,g,xcomo entradas e retornos [f,f,g,g(x)]. Clone essa função e aplique-a a si mesma, digamos , e ao espaço em branco. Este aplicativo não altera a pilha, para que possamos aplicar a função novamente quantas vezes quisermos.

Este programa imprime a sequência infinita 001011011101111..., onde o número de 1s sempre aumenta em um:

@?/!@>!??/!!>!+.!!.!!.!!.+>!.!!$$$$+$>!>!$>!>!+>!$>!>!>!+>!>!///!!>!>!>!.!!.!!.!!.!!.!!.!!.!!.!!.!!.!!+!!!!!

O repositório contém uma versão anotada.

Zgarb
fonte
Estou um pouco confuso aqui. Quando você escreve "absorve", como no comando shift, você quer dizer pops ou aplicado pelo comando apply?
precisa saber é o seguinte
11
Além disso, eu realmente não tenho certeza pelas suas especificações de como a cadeia deve funcionar. Você poderia esclarecer com um exemplo, por favor?
tecywiz121
@ tecywiz121 É assim que eu entendo: digamos que você tenha duas funções no topo da pilha f(x1, x2, ..., xn)e g(y1, y2, ..., ym). A chamada .aparece nos dois e pressiona uma função h(z1, z2, ..., zn). Agora você pode consumir todos esses argumentos gradualmente fazendo curry com ele !. Após nessas aplicações, a função restante tinha apenas um argumento e, nesse ponto, calcula f(z1, z2, ..., zn)(ou seja, faplica-se a todos os argumentos que você inseriu), o que gera alguns novos valores e, em seguida, consome imediatamente os mvalores da pilha e os chama g.
Martin Ender
@ MartinBüttner Se o Zgarb achar que está alinhado com as regras, você poderá usar um segundo parâmetro de entrada que define o tamanho máximo da saída. Isso também seria uma solução para o problema de avaliação preguiçosa.
Random #
@ tecywiz121 .funciona exatamente como Martin descreveu, exceto que, se fretornar uma lista menor que mvalores, o resultado será indefinido (a composição tem aridade n, portanto, não pode ser consumido mais argumentos da pilha). Essencialmente, a saída de fé usada como uma pilha temporária, na qual gsão pressionados e aplicados os mtempos de utilização !, e o resultado disso é adicionado à pilha principal.
Zgarb

Respostas:

12

Python 2, 752 667 534 506 445 436 427 404 398 393 bytes

Isso não é nada curto ... mas eu fiz o meu melhor. Qualquer sugestão de golfe seria muito apreciada ...

EDIT6: Agora é um script em vez de uma função. Salve-o em um arquivo (shift.py, forex) e execute-o com $ python shift.py '<my_input>'. Coloque a entrada entre aspas simples, ou o bash ficará louco com os caracteres de entrada.

EDIT7: Aaaaaaand ... não é mais legível. Mas me livrei de mais 23 bytes, então isso é bom, eu acho? Vou postar uma versão não-destruída também.

EDIT8: Mais um golfe, graças a @Zgarb.

k,d=[],[]
u=k.append
def z(f,a=1):f.a=a;return f
exec "i=!x:x(*map(k.pop,[-1]*x.a)));e=dict(zip('?+>/$.@',[0,!x:u(x)<u(x)),!x:u(!a,*_:x(*_)<u(a),x.a+1))),!x,y,z:u((z,y)[x<1]),3),!x,y:u(!*_:x(y,*_),x.a-1))if x.a>1 else x(y),2),!x,y:u(!*_:x(*_)<i(y),x.a)),2),!x:d.append(`+(x>0)`)<u(x))]))".replace('!',"z(lambda ")
for _ in raw_input():
 try:[i,u][_ in e](e.get(_,e['$']))
 except:break
print d

EDIT: obrigado a @DLosc pela ajuda no golfe! Conseguiu reduzi-lo em 85 bytes.

EDIT2: corte uma tonelada de invólucros desnecessários e solte-o em outros 133 bytes!

EDIT3: ... e mais 28 graças a @ Sp3000 e @orlp no chat!

EDIT4: com a ajuda do @orlp & @ Sp3000, removeu todos os decoradores e agora é 61 bytes mais curto.

EDIT5: ajuda meeeeee, eu não consigo parar de jogar isso .... mais 9 bytes se foram. Livrar-se da declaração de impressão final salvaria outros 7, mas se você executar m () em um loop, toda a saída estará na mesma linha ... tudo bem?

Aqui está uma versão não destruída:

stack = []
push = stack.append

def arity(func,a=1): #give each of our functions an arity
    func.arity = a
    return func

def do(func): ##pop the args off the stack, then call the function
    args = map(stack.pop,[-1]*func.arity)
    func(*args)

def call(func,arg): #apply is just do(call)
    if func.arity == 1:
        func(arg)
    else:
        def curried(*a): #a quick little currier
            func(arg, *a)
        curried = arity(curried, func.arity - 1)
        push(curried)

def clone(arg):
    push(arg)
    push(arg)

def shift(func):
    def shifted(a, *arg):
        func(*arg)
        push(a)
    shifted = arity(shifted, func.arity + 1)
    push(shifted)

def fork(a, b, c):
    if a == 0:
        push(b)
    else:
        push(c)

def chain(func, gunc):
    def composition(*args):
        func(*args)
        do(gunc)
    composition = arity(composition, func.arity)
    push(composition)

def say(arg):
    print '10'[arg == 0],
    push(arg)

commands = {'?': 0,
            '+': arity(clone),
            '>': arity(shift),
            '/': arity(fork, 3),
            '$': arity(call, 2),
            '.': arity(chain, 2),
            '@': arity(say)}

def interpret(input_string):
    for command in input_string:
        try:
            if command == '!':
                do(call)
            else:
                push(commands[command])
        except RuntimeError: #this handles max recursion depth errors
            break            # for infinite programs
    print

if __name__ == "__main__":
    interpret(raw_input())

A idéia básica é que a lista python funcione muito bem como uma pilha e, ao armazenar u=k.append, não apenas salvo caracteres, mas também posso usar @ucomo decorador para empurrar funções (não mais!).

Como as duas funções que atuam nas funções da n-arity precisam aceitar um número arbitrário de argumentos, eu tive que usar *args, o que significava que meu plano original de rastrear f.func_code.co_argcount precisava ser substituído por uma arity atributo decorador .

Em termos de manipulação de programas infinitos, o intérprete é executado até atingir a profundidade recursiva máxima; o manipulador RuntimeError na parte inferior faz com que saia silenciosamente nesse ponto e imprime a sequência de saída atual.

Casos de teste:

>>> tests
['?@!@@!', '$+.!!+!!', '?@$..!!+.!!+>!.!!!!+?/!!!@!@>!!!', '?@+$>!>!.!!+>!.!!///!!>!>!.!!+!!!!', '?@+$>!>!.!!+>!.!!///!!>!>!.!!+!!!!!!!', '?@+$>!>!.!!+>!.!!///!!>!>!.!!+!!!!!!!!!!', '?@+$>!>!.!!+>!.!!///!!>!>!.!!+!!!!!!!!!!!!!', '@?/!@>!.!!??/!!>!.!!+.!!.+>!.!!$$.!!$.!!$.!!+.!!$>!>!.!!$>!>!.!!+>!.!!$>!>!>!.!!+>!>!.!!///!!>!>!>!.!!+!!!!!']
>>> for t in tests: m(t)
0 1

0 0 1 0
0
0 0
0 0 0
0 0 0 0
0 0 1 0 1 1 0 1 1 1 0 1 1 1 1 0 1 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
sirpercival
fonte
11
Minha primeira reação: @ _ @ Sério, porém, um bom trabalho - colocar funções reais na pilha é uma solução realmente interessante. Algumas dicas: 1) Os operadores ternários geralmente podem ser encurtados de uma maneira ou de outra . 2) Você pode substituir ['1','0'][...]com apenas '10'[...]. 3) Por que x is 0e não x==0(ou x<1)? 4) Não se preocupe em especificarRuntimeError , apenas exceptfará. 5) Como você está usando o Python 2, as guias e os espaços contam como diferentes níveis de recuo - facilmente, mas devem economizar ~ 25 bytes.
DLosc
11
Você deve poder cortá-lo para x.a==1and x(y)or u(a(x.a-1)(b.partial(x,y)))- os operadores lógicos ainda estão em curto-circuito, mas usam menos caracteres que o ternário. Salve outro byte usando x.a-1como condição (0 / false sex for 1, diferente de zero / verdade de outra forma) e trocando o 'depois' e 'else' expressões: x.a-1and u(a(x.a-1)(b.partial(x,y)))or x(y). (Tenho a minha golf um pouco mais agora que você me ... passou; ^))
DLosc
11
Depois de encontrar um problema semelhante ao meu, entendo o que está falhando agora - se x.a==1é verdade, mas x(y)retorna algo falso, ele tenta avaliaru(...) também. Mas parece que você não precisa salvar os míseros 3 bytes que lhe dariam! Eu concordo, senhor: você me superou.
DLosc
11
Apenas uma queixa: o formato de saída especificado não possui espaços - você pode resolver de várias estratégias , sem ter certeza de qual é o menor. Obviamente, o seu programa lida com o RuntimeErrorenquanto o meu apenas pede ao usuário que redirecione o stderr ... então provavelmente estamos até discutindo. ; ^)
DLosc 23/04
11
Para que *_servem as lambdas?
mbomb007
4

Ghostscript

Ainda não joguei golfe, pois ainda preciso trabalhar a função de análise.

Esta implementação usa _e em :vez de >e /, e requer que todos os caracteres do programa sejam separados por espaços. Isto porque >e /não são nomes válidos em Postscript, e os operadores não são auto-delimitação, mas isso será corrigido quando eu escrever o analisador.

A primeira parte do código deve ser bastante transparente, pois está apenas repetindo as definições das funções do operador. A mágica acontece na definição de !.

/switch {
    /y exch def
    /x exch def
    {x} {y} ifelse
} bind def

/unwrap {
    dup type (arraytype) eq {aload pop} if
} bind def

/! { % IN: <x> <f> OUT: <g>|<f(x)>
    [ 3 1 roll unwrap] cvx %prefix argument into function
    dup /fun exch def %bind

    [ count 1 roll ] { %run the function sandboxed so it can't take any additional args
        2 dict begin
        /=only {} def % suppress output
            {
                fun
            } stopped /err exch def clear err
        end
    } .runandhide


    exch {
        $error /errorname get
        (stackunderflow) ne {
            handleerror
        } if

        $error /newerror false put

        unwrap
    } {
        unwrap exec
    } ifelse
} def

/? 0 def
/+ {{dup}} def
/_ {{/f exch def pop f}} def % using _ instead of >
/: {{? ne 3 1 roll switch}} def % using : instead of /
/$ {{!}} def
/. {{/g exch def exec g}} def 
/@ {{dup ? eq {0}{1} ifelse =only}} def

O caminho ! obras é simples: Em primeiro lugar, ele adiciona argumento xpara fprefixando xao conteúdo de f, empurrando-o de volta na pilha, e nomear uma cópia do resultado fun.

Em seguida, agrupa toda a pilha como uma matriz. .runandhideé uma extensão do Ghostscript para executar código em área restrita, ocultando o conteúdo da matriz anterior do procedimento em que é chamado. odict comando envia um novo dicionário à pilha de dict, restringindo o escopo dos nomes definidos atéend ele retorne. Ele também substitui =only(o operador de saída que eu uso @) por um fictício, suprimindo a saída durante a execução do teste. stoppedé o equivalente PostScript da tryinstrução encontrada em outros idiomas e retorna true se seu procedimento gerou um erro e false se foi executado até a conclusão.

Após a funconclusão da execução de avaliação , o programa restaura a pilha original da matriz oculta e, sefun concluída sem erros, a executa de verdade, mantendo a saída.

AJMansfield
fonte
2

Python3, 685 670 634 633 bytes

Tenho certeza de que esta é a coisa mais longa que já joguei. Costumava ser um pouco legível, mas seguir o conselho de @ sirpercival eliminou essa desvantagem!

from re import*
E,*k="E"
P="e(k.pop(),k.pop())"
def H(a,b):global k;k+=list(a)+[N(b)];exec("k+=%s;"%P*Z(N(b)));return[]
def e(a,b):a=sub("(?<!\d)0",repr(N(b,1)).replace("\\",r"\\"),a,1);return Z(a)and[a]or list(eval(a))
D=list(zip("ilhydsSNZ",[3,2,2]+[1]*6,sub("A","N(a)",',b,c:[N([b,c][a>E])]|,b:e(A,N(b))|,b:["H(%s,%s)"%(A,repr(b))]|:print(0+(a>E),end="")or[A]|:[A]*2|:["S(0,%s)"%A]|,b:b+[A]|,b=-1:sub("\d+",lambda m:str(int(m.group())+b),a)|:len(split("\D0",a))-1').split("|")))
for n,r,f in D:exec(n+"=lambda a"+f)
F=dict(zip("/$.@+>?!",D))
for z in input():n,r,f=F[z];k+=z!="!"and[[n+"(%s)"%",".join("0"*r),E][z=="?"]]or eval(P)

ké a pilha, que contém funções representadas como seqüências de caracteres "h(0,0)"(que é c h ain ). Quando uma função é passada como um argumento para outra função, torna-se repr'd e todos os números incrementado: "h('h(1,1)',0)". Depois que todos os 0s são substituídos em uma função, a coisa toda é passada paraeval , chamando a função Python apropriada - a maioria das quais são funções lambda geradas a partir da grande string na linha 6 pela execlinha 7.

Obter vários níveis de funções aninhadas incrementadas, citadas e escapadas adequadamente foi a maior dor de cabeça. Eu poderia economizar um pouco mais em operações regex se pudesse assumir que o aninhamento de funções não avançaria mais que 9 níveis, mas, como apontado nos comentários, isso provavelmente não é uma suposição segura.

Versão anterior ungolfed do código:

from re import *
E="E"
stack=[]

clone=lambda a:[unnest(a)]*2
shift=lambda a:["shifted(0,%s)"%unnest(a)]
fork=lambda a,b,c:[unnest(c if a!=E else b)]
call=lambda a,b:apply(unnest(a),unnest(b))
chain=lambda a,b:["chained(%s,%s)"%(unnest(a),repr(b))]
def say(a):
 print(1 if a!=E else 0,end="")
 return [unnest(a)]

shifted=lambda a,b:b+[unnest(a)]
def chained(a,b):
 global stack
 stack+=list(a)+[unnest(b)]
 exec("stack+=apply(stack.pop(),stack.pop());"*zeros(unnest(b)))
 return []

nest=lambda a,direction:sub("\d+",lambda m:str(int(m.group())+direction),a)
unnest=lambda a:nest(a,-1)
zeros=lambda a:len(split("\D0",a))-1
def apply(a,b):
 a=sub("(?<!\d)0",repr(nest(b,1)).replace("\\",r"\\"),a,1)
 return [a] if zeros(a) else list(eval(a))

functions=dict(zip("+>/$.@",zip(["clone","shift","fork","call","chain","say"],[1,1,3,2,2,1])))

for cmd in input():
 if"!"==cmd:
  stack+=apply(stack.pop(),stack.pop())
 elif"?"==cmd:
  stack+=[E]
 else:
  name,arity=functions[cmd]
  stack+=[name+"(%s)"%",".join("0"*arity)]

A única falha em potencial desta implementação é que ela usa recursão; portanto, programas que deveriam ser infinitos atingem a profundidade máxima da recursão rapidamente. (Você provavelmente deseja redirecionar o stderr quando executar um programa infinito - caso contrário, o rastreamento da pilha irá inundar a saída real.) Fora isso, tudo parece estar funcionando.

DLosc
fonte
Você poderia escrever um programa que gere o programa acima e o execute? Você tem muitos códigos recorrentes que devem ser compactados como lambda ae k.pop().
mbomb007
@ mbomb007 ... Acho que meu cérebro explodiria. (Mas veja edição recente - Fiz a k.pop()situação um pouco menos repetitivo, de qualquer maneira.)
DLosc
você pode executar o truque exec / translate para todas essas lambdas? colá-los todos em uma corda?
sirpercival
Um outro comentário: Eu duvido que você pode confiar em função de nidificação <= 9 com esta linguagem
sirpercival
@sirpercival Sim, eu estava pensando em tentar isso. E não, suponho que não. : ^ P
DLosc 23/04
1

Ceilão, 1167 1057 1031

Eu não entendo tão curto quanto as versões python mono-tipadas ...

import ceylon.language.meta.model{N=Function}import ceylon.collection{H=HashMap}interface D of F|b{}object b satisfies D{}class F(shared Integer a,[D+](D+)f,[D*]c=[])satisfies D{shared[D+]o(D i){[D+]s=[i].prepend(c);return a==1then f(*s)else[F(a-1,f,s)];}shared[D+]y([D+]i){return f(*i.prepend(c));}}F m<A>(N<[D+],A>f)given A satisfies[D+]=>F(f.parameterTypes.size,(D+i)=>f.apply(*i));[D,D]e(D x)=>[x,x];[F]t(F f){[D+]g(D+i){assert(is[D+]r=i.rest);return[i[0],*f.y(r)];}return[F(f.a+1,g)];}[D]k(D a,D d,D c)=>a==b then[d]else[c];[D+]l(F a,D x)=>a.o(x);[F]n(F f,F g){[D+]h(D+i){[D+]r=f.y(i);assert(is[D+]d=r[0:g.a]);return g.y(d).append(r[g.a...]);}return[F(f.a,h)];}[D]y(D x){process.write(x==b then"0"else"1");return[x];}class I(){variable D[]s=[];value c=H{'?'->b,'+'->m(`e`),'>'->m(`t`),'/'->m(`k`),'$'->m(`l`),'.'->m(`n`),'@'->m(`y`)};shared void r(Character i){if(i=='!'){assert(is F f=s[0],is D x=s[1]);s=f.o(x).append(s[2...]);}else{assert(is D d=c[i]);s=[d].append(s);}}}shared void z(){process.readLine()?.collect(I().r);}

Aqui está uma versão formatada (e comentada) do mesmo código (com espaços / novas linhas / comentários, torna-se 4867 bytes):

import ceylon.language.meta.model {
    N=Function
}
import ceylon.collection {
    H=HashMap
}
//↑ Import of stuff we need – with a shorter alias.
// (The comment is down here due to a bug in my comment and space
//  remover – it doesn't remove a comment if it is the first token
//  at all.)

// Our data items are either functions or blanks.
interface D of F | b {}

// There is no point in having many blanks – so here a singleton.
object b satisfies D {}

// The function class. Our functions take a number of data items,
// and return a number of data items.
// We know the arity a, and have also an actual function f, and a number
// or already collected arguments.
class F(shared Integer a, [D+](D+) f, [D*] c = [])
        satisfies D {
    // apply once (= collect one parameter). Returns either the result,
    // or a function with arity one less.
    shared [D+] o(D i) {
        [D+] s = [i].prepend(c);
        return a == 1 then f(*s) else [F(a - 1, f, s)];
    }
    // apply fully (= with all needed parameters).
    // The input size should equal the arity.
    shared [D+] y([D+] i) {
        // merge collected and input arguments.
        return f(*i.prepend(c));
    }
}
// creates a shift function from a ceylon function,
// deriving the arity using reflection.
F m<A>(N<[D+],A> f)
        given A satisfies [D+]
        => F(f.parameterTypes.size, (D+ i) => f.apply(*i));

//
// clone: a unary function that duplicates its input: any value x is mapped to [x,x].
//
[D, D] e(D x) => [x, x];

//
// shift: a unary function that takes in an n-ary function f, and returns an
// (n+1)-ary function g that ignores its first argument x, calls f on the
// remaining ones, and tacks x in front of the result. For example,
// shift(clone) is a binary function that takes inputs a,b and returns [a,b,b].
//
[F] t(F f) {
    [D+] g(D+ i) {
        assert (is [D+] r = i.rest);
        return [i[0], *f.y(r)];
    }
    return [F(f.a + 1, g)];
}

//
// fork: a ternary function that takes three inputs a,d,c, and returns [d] if a is a blank,
// and [c] otherwise.
//
[D] k(D a, D d, D c) => a == b then [d] else [c];

//
// call: a binary function that pops a function f and a value x,
//        and applies f to x exactly as ! does.
//
[D+] l(F a, D x) => a.o(x);

//
// chain:  a binary function that pops two functions f and g, and returns their composition:
//         a function h that has the same arity as f, and which takes its inputs normally, applies
//         f to them, and then fully applies g to the result (calls it as many times as its arity
//         dictates), with unused items from the output of f remaining in the result of h. For
//         example, suppose that f is a binary function that clones its second argument, and
//         g is call. If the stack contains [f,g,a,b,c] and we do .!!, then it contains
//         [chain(f,g),a,b,c]; if we do !! next, then f is first applied to a,b, producing
//         [a,b,b], then g is applied to the first two elements of that since its arity is 2,
//         producing [a(b),b], and the stack will finally be [a(b),b,c].
//
[F] n(F f, F g) {
    [D+] h(D+ i) {
        // call f, remember the results.
        [D+] r = f.y(i);
        // first some results from f are the arguments to g:
        assert (is [D+] d = r[0:g.a]);
        // remaining results from f are passed back directly, with the results from g.
        return g.y(d).append(r[g.a...]);
    }
    return [F(f.a, h)];
}

//
// say: a unary function that simply returns its input, and prints 0 if it was a blank,
//      and 1 if it was a function.
// 
[D] y(D x) {
    process.write(x == b then "0" else "1");
    return [x];
}

//
// Interpreter class, which manages the stack and interprets the commands.
// Just call the r method with the individual command characters.
//
class I() {
    // The stack. The only variable in the whole program.
    variable D[] s = [];

    // a hash map of items to be pushed by commands, most build using the m function.
    // The apply command is not here, this is handled separately by the interpreter. 
    value c = H {
        '?'->b,
        '+'->m(`e`),
        '>'->m(`t`),
        '/'->m(`k`),
        '$'->m(`l`),
        '.'->m(`n`),
        '@'->m(`y`)
    };

    // Interprets one command, indicated by a character.
    // Will throw an AssertionError for unknown commands.
    shared void r(Character i) {
        if (i == '!') {
            assert (
                is F f = s[0],
                is D x = s[1]);
            // apply f on x, push the result onto a shortened version of the stack.
            s = f.o(x).append(s[2...]);
        } else {
            assert (is D d = c[i]);
            // push d on top of the stack.
            s = [d].append(s);
        }
    }
}

shared void z() {
    process.readLine()?.collect(I().r);
}

As funções clonar e, shift t, bifurcar k, chamar l, dizer ye encadearn usam a última letra dos nomes para a versão abreviada, porque isso causou menos colisões. (Curiosidades: fork foi originalmente definida da seguinte maneira: [Data] fork(Data a, Data b, Data c) => a == blank then [b] else [c];- quando eu renomeei blankpara b, isso quebrou, porque agora comparava os parâmetros aeb , em vez acom o branco Levei algum tempo para depuração.).

o z função é compartilhada porque meu IDE executa essas funções - a ferramenta de linha de comando também pode executar as não compartilhadas.

Paŭlo Ebermann
fonte
As versões em loop realmente lançam um StackOverflowError em algum momento, terminando então. A JVM não possui otimização de pilha de recursão (ou pelo menos nenhuma que funcionaria para o meu programa).
Paŭlo Ebermann