Escribir un intérprete de turno

10

EDITAR: Como algunos de ustedes sospecharon, había un error en el intérprete oficial: .se invirtió el orden de composición . Tenía dos versiones del intérprete y usé la incorrecta aquí. Los ejemplos también se escribieron para esta versión incorrecta. He arreglado el intérprete en el repositorio y los ejemplos a continuación. La descripción de >también fue un poco ambigua, así que lo arreglé. Además, las disculpas por haber tardado tanto, quedé atrapado en algunas cosas de la vida real.

EDIT2: Mi intérprete tuvo un error en la implementación .que se reflejó en los ejemplos (confiaban en un comportamiento indefinido). El asunto ha sido arreglado.

Introducción

Shift es un lenguaje de programación funcional esotérico que hice hace un par de años, pero que publiqué hoy. Está basado en la pila, pero también tiene curry automático como Haskell.

Especificación

Hay dos tipos de datos en Shift:

  • Funciones, que tienen una aridad positiva arbitraria (número de entradas) y que devuelven una lista de salidas. Por ejemplo, una función que duplica su única entrada tiene arity 1, y una función que intercambia sus dos entradas tiene arity 2.
  • Los espacios en blanco, que son todos idénticos y no tienen otro propósito que no ser funciones.

Un programa Shift consta de cero o más comandos , cada uno de los cuales es un único carácter ASCII. Hay 8 comandos en total:

  • !( aplicar ) muestra una función fy un valor xde la pila, y se aplica fa x. Si ftiene arity 1, la lista f(x)se agrega al frente de la pila. Si tiene arity, se empuja n > 1una nueva (n-1)función -ary ga la pila. Toma entradas y retornos .x1,x2,...,xn-1f(x,x1,x2,...,xn-1)
  • ?(en blanco ) empuja un espacio en blanco a la pila.
  • +( clone ) empuja a la pila una función unaria que duplica su entrada: cualquier valor xse asigna a [x,x].
  • >( shift ) empuja a la pila una función unaria que toma una nfunción -ary f, y devuelve una (n+1)función -ary gque ignora su primer argumento x, invoca flos restantes y agrega xdelante del resultado. Por ejemplo, shift(clone)es una función binaria que toma entradas a,by retornos [a,b,b].
  • /( fork ) empuja a la pila una función ternaria que toma tres entradas a,b,cy devuelve [b]si aestá en blanco, y de lo [c]contrario.
  • $( Llamada ) empuja a la pila de una función binaria que aparece una función fy un valor x, y se aplica fa xexactamente como !lo hace.
  • .( cadena ) empuja a la pila una función binaria que muestra dos funciones fy gdevuelve su composición: una función hque tiene la misma aridad que f, y que toma sus entradas normalmente, se aplica fa ellas y luego se aplica completamenteg al resultado (llamadas tantas veces como lo dicta su arity), con elementos no utilizados de la salida de fpermanecer en el resultado de h. Por ejemplo, suponga que fes una función binaria que clona su segundo argumento y ges call . Si la pila contiene [f,g,a,b,c]y nosotros .!!, entonces contiene [chain(f,g),a,b,c]; si lo hacemos a !!continuación, fprimero se aplica a a,b, produciendo[a,b,b], luego gse aplica a los primeros dos elementos de eso ya que su arity es 2, produciendo [a(b),b], y la pila finalmente lo será [a(b),b,c].
  • @( digamos ) empuja una función unaria que simplemente devuelve su entrada e imprime 0si estaba en blanco y 1si era una función.

Tenga en cuenta que todos los comandos, excepto !simplemente insertar un valor en la pila, no hay forma de realizar la entrada, y la única forma de generar algo es usar @. Un programa se interpreta evaluando los comandos uno por uno, imprimiendo 0so 1s cada vez que se llama "say", y saliendo. Cualquier comportamiento no descrito aquí (aplicar un espacio en blanco, aplicar una pila de longitud 0 o 1, llamar "cadena" en un espacio en blanco, etc.) no está definido: el intérprete puede fallar, fallar silenciosamente, pedir información o lo que sea.

La tarea

Su tarea es escribir un intérprete para Shift. Debe tomar de STDIN, línea de comando o argumento de función un programa Shift para ser interpretado, e imprimir en STDOUT o devolver la salida resultante (posiblemente infinita) de 0sy 1s. Si escribe una función, debe poder acceder a las salidas de longitud infinita de alguna manera (generador en Python, lista diferida en Haskell, etc.). Alternativamente, puede tomar otra entrada, un número ny devolver al menos ncaracteres de la salida si es más larga que n.

El conteo de bytes más bajo gana, y las lagunas estándar no se permiten.

Casos de prueba

Este programa Shift imprime 01:

?@!@@!

Comenzando desde la izquierda: presione un espacio en blanco, presione say , luego aplique el say al espacio en blanco. Esto da salida 0. Luego, presione decir dos veces y aplique el segundo decir al primero. Esto da salida 1.

Este programa se repite para siempre, sin producir resultados:

$+.!!+!!

Presione call y clone , luego aplique chain a ellos (necesitamos dos !s ya que chain es una función binaria). Ahora la pila contiene una función que toma un argumento, lo duplica y llama a la primera copia del segundo. Con +!!, duplicamos esta función y la llamamos a sí misma.

Este programa imprime 0010:

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

Empuje un espacio en blanco y diga . Luego, componga una función binaria que copie su segundo argumento b, luego acopie el primero y lo componga consigo mismo, luego aplique la composición a la copia de b, regresando [a(a(b)),b]. Aplíquelo a say y blank, luego aplique say a los dos elementos restantes en la pila.

Este programa imprime 0. Para cada uno !!!que anexe, imprime un adicional 0.

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

Empuje un espacio en blanco y diga . Luego, componga una función ternaria que tome f,g,xcomo entradas y retornos [f,f,g,g(x)]. Clone esa función y aplíquela a sí misma, digamos , y al espacio en blanco. Esta aplicación no cambia la pila, por lo que podemos volver a aplicar la función tantas veces como queramos.

Este programa imprime la secuencia infinita 001011011101111..., donde el número de 1s siempre aumenta en uno:

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

El repositorio contiene una versión anotada.

Zgarb
fuente
Estoy un poco confundido aquí. Cuando escribe "toma" como en el comando shift, ¿quiere decir pops o quiere decir aplicado por el comando apply?
tecywiz121
1
Además, no estoy seguro de su especificación sobre cómo se supone que funciona la cadena. ¿Podría aclararlo con un ejemplo por favor?
tecywiz121
@ tecywiz121 Así es como lo entiendo: digamos que tienes dos funciones en la parte superior de la pila, f(x1, x2, ..., xn)y g(y1, y2, ..., ym). Llamar los .hace saltar a ambos y empuja una función h(z1, z2, ..., zn). Ahora puedes consumir todos esos argumentos al graduarlos gradualmente !. Después de ntales aplicaciones, la función restante tenía solo un argumento, y en ese punto se computa f(z1, z2, ..., zn)(es decir, se faplica a todos los argumentos en los que se cursó), lo que empuja algunos valores nuevos, y luego consume inmediatamente los mvalores de la pila y los invoca g.
Martin Ender
@ MartinBüttner Si Zgarb cree que está en línea con las reglas, podría usar un segundo parámetro de entrada que defina el tamaño máximo de la salida. Esto también sería una solución para el problema de la evaluación diferida.
randomra
@ tecywiz121 .funciona exactamente como Martin describió, excepto que si fdevuelve una lista de mvalores inferiores a , el resultado no está definido (la composición tiene aridad n, por lo que no puede comer más argumentos de la pila). Esencialmente, la salida de fse usa como una pila temporal, en la que gse empuja y se aplica mveces !, y el resultado se agrega a la pila principal.
Zgarb

Respuestas:

12

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

Esto no es para nada corto ... pero hice lo mejor que pude. Cualquier sugerencia de golf sería muy apreciada ...

EDITAR6: Esto es ahora un script en lugar de una función. Guárdelo en un archivo (shift.py, forex), luego ejecútelo con $ python shift.py '<my_input>'. Asegúrese de poner la entrada entre comillas simples, o bash se volverá loco con los caracteres de entrada.

EDIT7: Aaaaaa y ... ya no es legible. Pero me deshice de 23 bytes más, así que eso es bueno, supongo. También publicaré una versión sin golf.

EDITAR8: Un golf más, gracias 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

EDITAR: ¡gracias a @DLosc por la ayuda en el golf! Logró reducirlo en 85 bytes.

EDIT2: recorta una tonelada de envoltorios innecesarios y lo dejó caer en otros 133 bytes.

EDIT3: ... y 28 más gracias a @ Sp3000 y @orlp en el chat.

EDIT4: con la ayuda de @orlp & @ Sp3000, eliminé todos los decoradores y ahora es 61 bytes más corto.

EDIT5: ayúdame, no puedo dejar de jugar al golf ... 9 bytes más desaparecidos. Deshacerse de la declaración de impresión final ahorraría otros 7, pero si ejecuta m () en un bucle, toda la salida está en la misma línea ... ¿está bien?

Aquí hay una versión sin golf:

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())

La idea básica es que la lista de Python funciona muy bien como una pila, y al almacenar u=k.append, no solo guardo los caracteres, sino que también puedo usarlos @ucomo decorador para empujar funciones (¡ya no más!).

Como las dos funciones que actúan sobre las funciones de n-arity necesitan poder aceptar un número arbitrario de argumentos, tuve que usar *args, lo que significaba que mi plan original de seguimiento f.func_code.co_argcount tenía que ser reemplazado por un arity atributo decorador .

En términos de manejo de programas infinitos, el intérprete se ejecuta hasta alcanzar la profundidad recursiva máxima; el controlador RuntimeError en la parte inferior hace que salga silenciosamente en ese punto e imprime la cadena de salida actual.

Casos de prueba:

>>> 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
fuente
1
Mi primera reacción: @ _ @ En serio, sin embargo, buen trabajo: poner las funciones reales en la pila es una solución realmente ordenada. Algunos consejos: 1) Los operadores ternarios generalmente se pueden acortar de una forma u otra . 2) Se puede reemplazar ['1','0'][...]con solo '10'[...]. 3) ¿Por qué x is 0y no x==0(o x<1)? 4) No te molestes en especificar RuntimeError, solo exceptservirá. 5) Dado que está utilizando Python 2, las pestañas y los espacios cuentan como diferentes niveles de sangría, feo, pero debería ahorrarle ~ 25 bytes.
DLosc
1
Debería poder cortarlo: x.a==1and x(y)or u(a(x.a-1)(b.partial(x,y)))los operadores lógicos todavía están en cortocircuito, pero usan menos caracteres que el ternario. A continuación, guarde otro byte mediante el uso de x.a-1la condición (0 / false si xes 1, distinto de cero / verdadero lo contrario) y el canje de la 'continuación' y 'lo demás' expresiones: x.a-1and u(a(x.a-1)(b.partial(x,y)))or x(y). (Tengo que jugar al golf un poco más ahora que me has pasado ...; ^))
DLosc
1
Después de tener un problema similar con el mío, entiendo lo que está fallando ahora; si x.a==1es cierto pero x(y)devuelve algo falso, también intenta evaluarlo u(...). ¡Pero parece que no necesita guardar los 3 bytes miserables que le habrían dado! Lo admito, señor: me ha superado.
DLosc
1
Solo una objeción: el formato de salida especificado no tiene espacios; puede resolverlo mediante varias estrategias , sin estar seguro de cuál es el más corto. Por supuesto, su programa maneja el RuntimeErrortiempo mientras que el mío solo le pide al usuario que redirija stderr ... por lo que probablemente incluso tengamos dudas. ; ^)
DLosc
1
¿Para qué sirve *_las lambdas?
mbomb007
4

Ghostscript

Todavía no he jugado al golf, ya que todavía necesito resolver la función de análisis.

Esta implementación usa _y en :lugar de >y /, y requiere que todos los caracteres del programa se separen con espacios. Esto se debe >y /no son nombres válidos en Postscript, y los operadores no son auto-delimitación, pero este problema se solucionará cuando escribo el analizador.

La primera parte del código debe ser bastante transparente, ya que simplemente está repitiendo las definiciones de las funciones del operador. La magia ocurre en la definición 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

La forma en que !funciona es simple: en primer lugar, agrega argumentos xal fprefijar xel contenido de f, empujarlo hacia atrás en la pila y nombrar una copia del resultado fun.

Luego envuelve toda la pila como una matriz. .runandhidees una extensión de Ghostscript para ejecutar código de espacio aislado, ocultando el contenido de la matriz anterior del procedimiento en el que se invoca. El dictcomando empuja un nuevo diccionario en la pila de dict, reduciendo el alcance de los nombres definidos dentro hasta que endlo desactive. También reemplaza =only(el operador de salida que uso @) con uno ficticio, suprimiendo la salida durante la ejecución de prueba. stoppedes el equivalente PostScript de la trydeclaración que se encuentra en otros lenguajes, y devuelve verdadero si su procedimiento arrojó un error, y falso si se ejecutó hasta su finalización.

Una vez que se completa la ejecución de prueba fun, el programa restaura la pila original de la matriz oculta y, si se funcompleta sin error, la ejecuta de manera real, manteniendo la salida.

AJMansfield
fuente
2

Python3, 685 670 634 633 bytes

Estoy bastante seguro de que es lo más largo que he jugado al golf. Solía ​​ser algo legible, pero seguir los consejos de @ sirpercival ha eliminado ese inconveniente.

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)

kes la pila, que contiene funciones representadas como cadenas como "h(0,0)"(que es c h ain ). Cuando una función se pasa como argumento a otra función, se pone repr'd y todos los números incrementa: "h('h(1,1)',0)". Una vez que todos los 0s se reemplazan en una función, todo se pasa a eval, llamando a la función Python apropiada, la mayoría de las cuales son funciones lambda generadas a partir de la cadena grande en la línea 6 por la execlínea 7.

Obtener múltiples niveles de funciones anidadas incrementadas, citadas y escapadas correctamente fue el mayor dolor de cabeza. Podría ahorrar un poco más en las operaciones de expresiones regulares si pudiera asumir que el anidamiento de funciones no avanzará más allá de los 9 niveles, pero como se señaló en los comentarios, probablemente no sea una suposición segura.

Versión anterior sin codificar del 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)]

El único defecto potencial de esta implementación es que utiliza la recursividad, por lo que los programas que deberían ser infinitos alcanzan la profundidad máxima de recursión con bastante rapidez. (Probablemente desee redirigir stderr cuando ejecuta un programa infinito; de lo contrario, el seguimiento de la pila inundará la salida real). Aparte de eso, todo parece estar funcionando.

DLosc
fuente
¿Podría escribir un programa que genere el programa anterior y luego lo ejecute? Tienes muchos códigos recurrentes que deberían ser comprimibles como lambda ay k.pop().
mbomb007
@ mbomb007 ... Creo que mi cerebro explotaría. (Pero vea la edición reciente: de k.pop()todos modos, hice la situación un poco menos repetitiva)
DLosc
¿Puedes hacer el truco exec / translate para todas esas lambdas? pegarlos todos en una cuerda?
sirpercival
otro comentario: dudo que pueda confiar en la función de anidamiento <= 9 con este idioma
sirpercival
@sirpercival Sí, estaba pensando en intentarlo. Y no, supongo que no. : ^ P
DLosc
1

Ceilán, 1167 1057 1031

No lo entiendo tan corto como las versiones de 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);}

Aquí hay una versión formateada (y comentada) del mismo código (con espacios / líneas nuevas / comentarios se convierte en 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);
}

Las funciones clone e, shift t, fork k, call l, say yy chain nusan la última letra de los nombres para la versión abreviada, porque eso dio menos colisiones. (Trivia: la bifurcación se definió originalmente de esta manera: [Data] fork(Data a, Data b, Data c) => a == blank then [b] else [c];cuando cambié el nombre blankde besto, esto se rompió, porque ahora comparó los parámetros ay, en bcambio, acon el espacio en blanco. Me llevó algo de tiempo depurar).

La zfunción se comparte porque mi IDE ejecuta esas funciones: la herramienta de línea de comandos también puede ejecutar funciones no compartidas.

Paŭlo Ebermann
fuente
Las versiones en bucle en realidad arrojarán un StackOverflowError en algún momento, terminando entonces. La JVM no tiene optimización de pila de recursión (o al menos ninguna que funcione para mi programa).
Paŭlo Ebermann