Análisis semántico
Se ejecuta después del análisis sintáctico en la compilación por defecto y en
--print-types; las acciones solo sintácticas lo omiten. Los errores detienen
el lote afectado antes de LLVM; los diagnósticos conservan los orígenes de
macros/inclusiones, y las entradas posteriores se siguen diagnosticando.
Comprobaciones de módulo y de llamadas
- La declaración de módulo es obligatoria y única; aridades de función 0..255; sin definiciones duplicadas; cada exportación existe (una exportación repetida genera una advertencia, como en OTP). Los nombres entre comillas o Unicode conservan su identidad exacta.
- Se comprueban todas las funciones, incluido el código no usado e inalcanzable.
- Las llamadas se resuelven dentro del lote por módulo/nombre/aridad. Las llamadas remotas (incluidas las autocualificadas) requieren exportaciones. Las funciones llamadas ausentes o privadas y los módulos duplicados son errores.
fun F/Adebe nombrar una función del módulo (function F/A undefined);fun M:F/Ay las llamadas a valores (F(Args)) no se resuelven en tiempo de compilación (funs). Cada valor fun distinto obtiene una entrada enModule::funstras el análisis de vinculaciones.- Se acepta la recursión propia, mutua y entre módulos. El grafo de llamadas se divide en componentes fuertemente conexas (Tarjan iterativo) en orden de función llamada antes que llamadora; una componente es recursiva cuando tiene varios miembros o un miembro se llama a sí mismo.
Behaviours
Sigue los
behaviours
de OTP, erl_internal:add_predefined_functions y el check_behaviour de
erl_lint.
- Un módulo que declara
-callbackrecibe la función predefinida y exportadabehaviour_info/1de OTP:callbacksda los callbacks declarados en el orden del código fuente,optional_callbackslos de las listas-optional_callbacksbien formadas (los callbacks cualificados con un módulo se omiten). El frontend la añade como función en la línea de-module(driver/predefined). Como en OTP, el propio código fuente del módulo no puede nombrarla: un-export, una llamada local,fun behaviour_info/1o un-specde ella es una función no definida; las llamadas remotas (también las autocualificadas) la alcanzan. Un-callbackjunto a unbehaviour_info/1escrito a mano es un error (cannot define callback attibute for F/A when behaviour_info is defined, con la grafía de OTP). -behaviour(M)y-behavior(M)pueden repetirse.Mse resuelve en el lote; un nombre que corresponde a un módulo de biblioteca (library.md) añade ese módulo igual que lo hace una llamada literal. Los diagnósticos usan el texto de OTP en el nombre del atributo:undefined callback function F/A (behaviour 'M')por cada callback obligatorio y no opcional que el módulo no exporta (ordenados);conflicting behaviours -- callback F/A required by both 'M2' and 'M1' (line L, column C)cuando un behaviour anterior también lo requiere (un callback opcional cuenta cuando se exporta);behaviour M undefinedcuandoMno está en el lote o no tienebehaviour_info/1(un behaviour de OTP que la biblioteca de Clause aún no proporciona, comoapplication), además de los errores de nombre de módulo de OTP para nombres que no pueden ser módulos (the module name must not be empty,... must be an atom, ...).
- Las advertencias no hacen fallar la compilación. Las opciones de
-compilenowarn_behaviours(toda la comprobación, incluidos los errores de nombre de módulo),nowarn_undefined_behaviour_func,nowarn_undefined_behaviourynowarn_conflicting_behaviourslas desactivan. - No se evalúa: un
behaviour_info/1exportado y escrito a mano de un módulo de behaviour, de modo que los callbacks de sus usuarios no se comprueban, y las advertencias de OTP sobre callbacks mal definidos u obsoletos nunca se producen (diferencias). - Evidencia:
tests/fixtures/lint(diagnósticos generados por OTP, prueba de CTestlint_diagnostics),executables_behaviours, casos de la CLIsemantic.
Importaciones
Sigue las reglas de importación de erl_lint.
-import(Module, [F/A, ...])convierte una llamada localF(...)en la llamada remotaModule:F(...): el módulo debe estar en el lote (un módulo de biblioteca se une a él como con una llamada literal) y exportar la función, o ser un módulo de builtins (-import(io, [format/2]),-import(erlang, [display/1])). Una importación tiene prioridad sobre un BIF autoimportado del mismo nombre.- Errores con el texto de OTP:
function F/A already imported from M(entonces se ignora todo el atributo),defining imported function F/A,creating a fun from imported name F/A is not allowed. Una función importada en un guard es una llamada de guard ilegal (salvo los BIF de guard de erlang). - Advertencia
import directive overrides auto-imported BIF F/A -- ...salvo queno_auto_importlo nombre o se indiquenowarn_bif_clash; Clause conoce los BIF autoimportados que implementa. - Evidencia:
executables_imports, casos de lintimports_conflicts,imports_fun,imports_bif, casos de la CLI semantic.
Funciones predefinidas
Sigue los módulos de OTP y
erl_internal:add_predefined_functions. El frontend añade module_info/0,1 a
cada módulo compilado (después de un behaviour_info/1 generado) como
funciones Erlang que devuelven datos literales (driver/predefined,
semantic/module_info); Module::predefined_ cuenta estas formas finales,
que siempre se exportan y se omiten de --print-types.
module_info()da[{module,_},{exports,_},{attributes,_},{compile,_},{md5,_}];module_info(Key)también responde afunctions,nifs([]) ynative(false), y lanzabadargpara cualquier otra clave.exports: las funciones exportadas en orden de definición (los escripts añadenmain/1), despuésbehaviour_info/1cuando se genera,module_info/0,module_info/1.functions: todas las funciones en orden de definición, después las mismas predefinidas.attributes: todos los atributos exceptomodule, las exportaciones e importaciones, las formas de tipo/spec/callback, los records,export_type,optional_callbacks,export_record,compile,file, la documentación yfeature, en el orden del código fuente; un valor que no es una lista se envuelve en una (-tags(a)es{tags,[a]}). Sin-vsn,{vsn,[N]}va primero, siendoNelmd5leído como entero sin signo big endian, como lo deriva OTP.md5: MD5 del código fuente impreso del módulo (print_source: macros expandidas, inclusiones insertadas, comentarios y disposición eliminados).compile:[{version,ClauseVersion},{options,[]},{source,AbsolutePath}].- A diferencia de
behaviour_info/1, el propio código fuente del módulo las ve: las llamadas locales yfun module_info/1funcionan. Definir cualquiera de ellas es un error (function module_info/0 already defined); exportar una, como cualquier exportación repetida, solo genera una advertencia (function module_info/0 already exported). - Los atributos son datos; solo
on_load,nifsy las opciones de-compileno inertes se rechazan por cambiar el comportamiento. - Las funciones generadas no tienen línea de código fuente (OTP no les da
ninguna): sin ubicaciones de depuración, comentarios de fuente en el IR ni
entradas en
--print-types.module_info/0contiene los mismos literales quemodule_info/1, así que no hace llamadas. - Evidencia:
executables_module_info(stdout de OTP; valores derivados por su forma), casos detests/fixtures/lintmodule_info_defined,exports_repeated.
Vinculaciones
Cada vinculación tiene una identidad relativa a la función
clause[N].local[M]. Las apariciones son definiciones, lecturas o
comprobaciones de igualdad exacta, etiquetadas con el contexto de
cabecera/guard/cuerpo. El análisis es determinista.
- Cada cláusula parte de un entorno vacío. Los candidatos de la cabecera recogen definiciones provisionales; los guards las leen; el cuerpo solo las ve si hay éxito.
_no vincula nada;_Namees una variable ordinaria. Un nombre repetido es una restricción de igualdad exacta.- Las coincidencias en el cuerpo visitan el lado derecho antes que el
izquierdo; las cadenas van de derecha a izquierda. Un patrón entre paréntesis
P1 = P2es un alias, no una secuencia. - Las expresiones hermanas leen los mismos nombres entrantes; sus definiciones
se exportan después de toda la expresión.
{X = 1, X}es una lectura no vinculada;{X = 4, X = 3}es legal y falla en tiempo de ejecución. - Las definiciones en el lado derecho de
andalso/orelseo dentro decatch Exprson inseguras después (vtunsafede OTP); los nombres vinculados antes de uncatchsiguen siendo utilizables. Todo nombre vinculado dentro de untryes inseguro después; las cláusulasofven los nombres del cuerpo, las cláusulas catch los ven como inseguros, y el cuerpo de after ve como inseguro todo nombre vinculado antes en el try. La variable de pila de una cláusula catch debe ser nueva (stacktrace_boundde OTP) y su guard no debe leerla (stacktrace_guard). Unmaybeno exporta nada: cada?=vincula para las expresiones siguientes del cuerpo, las cláusulaselseven los nombres del cuerpo como inseguros y todo nombre vinculado dentro es inseguro después. Los guards nunca publican vinculaciones; las coincidencias en guards son errores incluso cuando son inalcanzables. - Las claves de map solo leen vinculaciones entrantes; los tamaños de binary también leen segmentos anteriores del mismo binary (véase patrones).
- La expresión de escrutinio de un
casevincula en el ámbito que la contiene. Cada cláusula parte de ese ámbito; las definiciones de su patrón son provisionales hasta el guard, que solo lee. Los nombres vinculados por todas las cláusulas se exportan con una única identidad (las cláusulas posteriores reutilizan la identidad que una cláusula anterior dio al nombre); los nombres vinculados solo por algunas cláusulas, o inseguros en alguna, son inseguros después. Las exportaciones se unen de forma conservadora como enerl_lintde OTP (icrt_export); no se emite la advertencia de OTP cuando un patrón posterior coincide con un nombre exportado. Las cláusulas deifsiguen las mismas reglas con un guard y sin patrón. begin/endes una secuencia en el ámbito que la contiene.- Las lecturas no vinculadas, inseguras y de comodín son errores con
ubicación. Los mensajes conservan la redacción del compilador
(
unbound variable X,unsafe variable X); el corpus de vinculaciones comprueba cada uno frente a la claseunbound_var/unsafe_varde OTP. - Una comprehension evalúa cada calificador en orden en el ámbito que dejan los anteriores. Los patrones de los generadores vinculan nombres nuevos que ocultan a los externos (las lecturas dentro del patrón prefieren los nombres que ya ha vinculado); un grupo zip vincula todos sus patrones a la vez. Las plantillas leen como expresiones hermanas. El ámbito después de la comprehension es el de antes: sus nombres no están vinculados allí.
- Cada cláusula de un fun anónimo parte del ámbito en el punto del fun: los
nombres de la cabecera son nuevos y ocultan a los externos, los guards los
leen y nada vinculado dentro es visible después del fun. Toda definición
externa leída dentro se registra como una captura (
Function::captures, en orden de definición). El nombre de un fun con nombre es una definición más con la que empieza cada cláusula (Function::fun_names); nunca se captura. Las variables defun M:F/Ason lecturas de la expresión fun (Function::fun_operands).
Los recorridos son iterativos con un presupuesto por módulo de 1.000.000 de
unidades de trabajo, o 32 por expresión cuando eso es más (los analizadores
sintácticos generados, como erl_parse de OTP, necesitan unas 10 por
expresión). La resolución de servicios tiene un presupuesto del mismo tamaño;
examina las formas -compile e -import del módulo para conocer el estado de
autoimportación de un nombre una vez por nombre, no una vez por llamada (cada
una de las miles de cláusulas de erl_parse llama a hd/1). El agotamiento o
cualquier error semántico vacía las tablas de vinculación y normalización del
módulo.
Tipos y especificaciones
Un grafo de tipos privado representa todas las formas de tipo analizadas con
independencia de la disposición en el runtime: singletons, rangos,
contenedores, roles de campos de map, productos de función y aplicaciones no
resueltas. Las uniones se aplanan y eliminan duplicados; term() es el
máximo y none() el mínimo. Valores por defecto: 1.048.576 nodos, 256
miembros de unión, 200.000 elementos de trabajo por recorrido de un tipo
declarado (resolución, traducción, expansión) y 8.000.000 por pasada de
inferencia de todo el programa; una compilación de depuración de clau (sin
NDEBUG) permite 16.000 y 1.000.000 para que los programas grandes sigan
compilándose rápido, y puede ampliar más. El agotamiento amplía a term() con
un indicador visible y nunca estrecha una representación.
Los metadatos declarados (-type, -opaque, -nominal, -export_type,
-spec, -callback, -optional_callbacks) siguen la referencia de typespecs
de OTP y el comportamiento fijado de erl_lint/erl_internal/erl_types:
- Los alias locales pueden ocultar nombres de tipos builtin. Los tipos remotos
del lote deben estar exportados. Los tipos ausentes, los duplicados, los
metadatos mal formados, las variables de tipo que aparecen una sola vez, los
límites no válidos y las specs de funciones ausentes son errores. Los tipos
externos no disponibles generan una advertencia y se convierten en
term(). - Cada alias o sobrecarga tiene su propio ámbito de variables; los parámetros formales repetidos siguen la sustitución por el último argumento de OTP.
- Los alias recursivos siguen siendo referencias con nombre finitas; los cuerpos opacos solo se expanden en su módulo; los nombres nominales se conservan en todas partes, y solo la comprobación de especificaciones lee la definición de un tipo nominal, dentro de su módulo.
- La evaluación de constantes es exacta, limitada a 10.000 dígitos decimales por valor.
Inferencia
La inferencia es independiente de los tipos declarados y nunca confía en las specs.
-
Las entradas exportadas, y las entradas de funciones que nombra un
fun F/A, son términos arbitrarios. Una función a la que solo se entra mediante llamadas directas del lote (ni exportada ni nombrada por un fun) toma como entradas la unión de los hechos de los argumentos de sus puntos de llamada (semantic/types/inference_inputs), confrontados con los patrones de la cabecera de cada cláusula; las llamadas recursivas también cuentan. Las entradas empiezan ennone()(una función a la que nada llama nunca se ejecuta e infierenone()) y crecen a lo largo de las pasadas sobre el lote. Cada entrada sube una escala según cuántas veces haya cambiado: sus primeros 4 cambios unen, los 2 siguientes amplían como los resultados recursivos, los 2 siguientes también reducen aterm()los contenedores dentro de otro contenedor, y un cambio más daterm()(notificado como ampliado). Las entradas que no se han estabilizado tras 63 pasadas se convierten enterm()(notificadas como ampliadas). Una componente recursiva se considera costosa cuando una resolución gasta más de 2^17 uniones del retículo (contando las anidadas) y más de 1/16 del máximo de uniones gastadas por una pasada hasta entonces; cuando las entradas de sus miembros vuelven a cambiar, se convierten de inmediato enterm()(notificadas como ampliadas), de modo que las resoluciones posteriores ya no siguen las fluctuaciones de entrada de sus llamadores. -
Las pasadas posteriores a la primera solo recorren las componentes para las que ha cambiado algo que leen (
semantic/types/inference_incremental): una componente conserva sus resúmenes y hechos de expresiones mientras no cambien las entradas de sus miembros, todos los resúmenes que leyó (llamadas,fun F/A,apply) y las entradas de las funciones llamadas que volvió a evaluar, y los presupuestos de pasada y de reevaluación le respondan igual (el presupuesto de reevaluación lleva gastado lo mismo que al inicio de su último recorrido, o volvería a concederlo o denegarlo todo); su trabajo sigue contando para el presupuesto de la pasada. Los resultados son iguales a los de recorrer todas las componentes. La pasada final con entradasterm()las recorre todas. Tras una pasada, solo se vuelven a unir a partir de sus llamadas las entradas de las funciones recorridas en ella o llamadas por una función recorrida en ella; los hechos de argumentos y las entradas de las demás quedan como en la última unión. -
Los literales tienen hechos exactos: los enteros de cualquier tamaño y los caracteres son singletons, los átomos átomos singleton, los flotantes
float(), las cadenas listas no vacías de sus caracteres ([]para""), y-/+de un número literal conservan su hecho. Las tuplas conservan los hechos de sus elementos; los maps construidos con claves constantes (el hecho de un único valor: un entero, un átomo,[], o una tupla o map de estos) conservan el hecho del valor de cada clave, una clave dada dos veces conserva su último valor, y cualquier otra clave damap(). Una construcción de bitstring cuenta su tamaño: los caracteres literales, dimensionados y codificados en UTF exactamente, un segmento UTF de otro valor según su codificación (UTF-8: de 8 a 32 bits por bytes), y un segmentobinary/bitstringsegún el hecho de su valor o cualquier múltiplo de su unidad (<<1, Rest/binary>>esnonempty_binary()). Un operando que nunca produce un valor (none()) hace que la construcción seanone(). -
Los operadores y los builtins (
semantic/types/inference_operators) calculan su resultado a partir de los hechos de sus operandos. Los singletons enteros se pliegan exactamente hasta 4.096 bits por operando y resultado (incluidos los bignums; uno mayor esinteger()); los demás enteros usan aritmética de intervalos para+,-,*de rangos no negativos,bandcon un operando no negativo (X band 15es0..15),rempor un divisor acotado (X rem 10es-9..9),bnot,-unario yabs/1; un operando flotante dafloat(), uno desconocidonumber(), y/es siemprefloat(). Las comparaciones se pliegan cuando los valores de los operandos solo pueden compararse de una manera (átomos o enteros singleton, rangos de enteros disjuntos, números frente a no números) y sonboolean()en otro caso;and/or/xor/notyandalso/orelsecombinan los valores de verdad que pueden tener sus operandos (el operando derecho deandalsoes el resultado cuando el izquierdo estrue). Una tabla da a cada builtin del puente su resultado:pid()paraself/0yspawn/1,3,reference(),port(),non_neg_integer()para tamaños,string(),nonempty_string(),binary(),list(),tuple(),atom(),boolean()para las pruebas de tipo,true/okpara builtins con efectos secundarios, el mensaje para!ysend/2;trunc/1y similares conservan los operandos enteros; los builtins desconocidos sonterm(). Una operación que siempre lanza una excepción (1 + a,1 div 0,error/1,exit/1,throw/1,halt/0,1) esnone(), yerlang:raise/3solo devuelvebadarg. El plegado nunca cambia el código generado: el desbordamiento y los fallos siguen siendo resultados en tiempo de ejecución. -
Un cuerpo cuya expresión nunca termina (
none()) esnone(); también lo es una llamada con un argumento así. Los caminos que lanzan excepciones no añaden nada a una unión, así que una función que siempre lanza una excepción infierenone(). -
Las funciones de identidad/proyección conservan relaciones exactas con los argumentos, propagadas a través de llamadas locales y remotas anidadas con variables nuevas por llamada.
-
Contenedores (
semantic/types/inference_containers): una lista[E1, ..., En | T]une sus elementos delante del hecho de la cola (una lista propia cuando la cola lo es,nonempty_improper_list(H, T)cuando no es una lista,nonempty_maybe_improper_list()cuando es desconocida o puede ser impropia: una celda nunca está vacía, también en un patrón como[H | _]);++,--,hd/1,tl/1,element/2,setelement/3,tuple_to_list/1ymap_get/2leen y reconstruyen los hechos de los elementos miembro a miembro de una unión, sin que añada nada un miembro que lanzaría una excepción. Una actualización de map conserva las claves exactas (:=de una clave ausente elimina ese miembro) y damap()para un map desconocido. Los records de tupla son tuplas: la construcción rellena los valores por defecto (undefinedsi no hay ninguno), el acceso y la actualización leen y fijan el campo de las tuplas que coinciden, y#r.fes el índice del campo. Una comprehension de lista es una lista posiblemente vacía de los hechos de sus plantillas, una comprehension de binary cualquier número de copias del tamaño de su plantilla, y una comprehension de mapmap(). -
Funs (
semantic/types/inference_funs):fun F/Aes un fun de su aridad que devuelve el resultado inferido de la función (el de un builtin o, con una aridad variable,fun()),fun M:F/Auno que devuelveterm(), y un fun anónimo uno que devuelve la unión de los resultados de sus cláusulas, incluidos los valores capturados. Una llamada a un valor une los resultados de sus funs de esa aridad que seleccionan sus argumentos (term()para un fun desconocido,none()cuando ningún miembro puede llamarse así). Un fun anónimo vinculado entero a una variable y llamado a través de ella se evalúa de nuevo para esa llamada con sus patrones confrontados con los hechos de los argumentos, hasta 4 llamadas de profundidad, y los hechos de su primera evaluación se restauran después (Double = fun(Y) -> Y * 2 end, Double(3)es 6).apply(F, Args)(tambiénerlang:apply/2) es una llamada aFcon los argumentos que contieneArgs: los elementos de una lista propia literal o, si no, los de su hecho ([], una lista de posiciones conocidas, o tantas copias del elemento de una lista propia como tome la única aridad deF); un fun anónimo vinculado se evalúa de nuevo como antes (apply(fun kind/1, [1])seleccionakind(1)).apply(M, F, Args)cuyosMyFson átomos conocidos que nombran una función exportada del lote lee los tipos de función de esa función comofun F/A. Los argumentos de longitud desconocida, otros destinos yM:F(Args)con nombres variables siguen siendoterm(). Comofun F/Ayapply/3pueden nombrar una función inferida más tarde, las pasadas sobre el lote también se repiten hasta que cada una de esas lecturas haya visto el resultado final de su función; si no, una última pasada les da resultadosterm(). -
Las asignaciones de valor completo en el cuerpo y los alias copian el hecho del lado derecho (
Y = 42, Z = Y, id(Z)infiere 42); los patrones de tupla, lista, map y record de tupla dan a sus variables los hechos de las partes con las que coinciden, en las coincidencias del cuerpo, en las cláusulas decase(a partir de la expresión de escrutinio), en las cláusulasofde untry(a partir del valor de su cuerpo) y en los generadores (a partir de los elementos de su entrada o de las claves y valores del map). Los valores no demostrados siguen siendoterm()sin relaciones. -
Estrechamiento (
semantic/types/inference_narrowing,inference_scopes,meet): dentro de una cláusula de función,case,receiveo fun, cada patrón se intersecta con el valor con el que coincide (una forma literal, de tupla, lista, record de tupla, map o bitstring; el hecho de una variable vinculada), una variable usada como expresión de escrutinio de uncasese estrecha con él, y el guard estrecha las variables que prueba. Las pruebas de tipo (is_atom/1aatom(),is_boolean/1,is_integer/1,is_float/1,is_number/1,is_binary/1,is_bitstring/1,is_list/1amaybe_improper_list(),is_tuple/1,is_map/1,is_function/1,2,is_pid/1,is_port/1,is_reference/1,is_record/2,3a la tupla del record,is_map_key/2su map amap(), y los nombres de guard antiguos) se intersectan con el hecho de su argumento; las comparaciones con constantes enteras (<,=<,>,>=,==,=:=, a cualquier lado, incluidas las constantes plegadas) estrechan a un rango un valor ya demostrado entero, acumulándose los límites de una prueba (10 >= X, X >= 0es0..10); dos enteros demostrados que se comparan se estrechan mutuamente por sus límites (X > YconYen0..5hace queXsea al menos 1), y/=,=/=con una constante desplazan hacia dentro un límite del rango igual a ella. Un valor que puede ser un flotante u otro término no se estrecha. Una conjunción aplica cada prueba por turno, una disyunción une lo que demuestra cada alternativa, ynoty las pruebas falsas no demuestran nada. Una cláusula posterior a otra cuyos patrones son todos variables simples y cuyo guard entero era una única prueba de tipo ve ese valor sin la categoría probada; tras una única comparación con una constante entera, el mismo valor (una variable simple de esta cláusula) demostrado entero ve la comparación como falsa (f(N) when N >= 0 -> ...; f(N) when is_integer(N) -> ...: la segunda cláusula veneg_integer()). El operando derecho deandalso, la cláusulatruedecase Test ofy lo que sigue a un filtro de comprehension ven la prueba como verdadera. Una intersección vacía hace imposible la cláusula: no añade nada al resultado. Los hechos estrechados solo son válidos dentro de su cláusula u operando;catch, el operando derecho deorelsey cada cláusula restauran los hechos anteriores a ellos. Después de uncase,if,receive,tryomaybe, el hecho de una variable es la unión de sus hechos al final de cada cláusula que termina, de modo quecase X of forever -> ...; N when is_integer(N), N >= 0 -> ... enddejaXcomonon_neg_integer() | forever. -
tryymaybe: untryes la unión de los valores de sus cláusulasof(el de su cuerpo si no hayof) y de los valores de sus cláusulas catch; el cuerpo deafterno añade nada. Las cláusulasofparten de los hechos al final del cuerpo y confrontan su valor como las cláusulas decase(una cláusula imposible no añade nada); las cláusulas catch y el cuerpo deafterparten de los hechos anteriores altry, y un patrón de clase coincide conerror | exit | throw. Unmaybees la unión del valor de su cuerpo, de los valores de sus cláusulaselsey, sinelse, de los valores en los que pueden fallar sus coincidencias?=: el hecho del valor confrontado sin la forma del patrón cuando el patrón cubre toda su forma (variables nuevas usadas una vez, átomos y enteros literales,[], tuplas de estos), y si no el hecho completo. Cada patrón?=se intersecta con su valor y publica sus variables para el resto del cuerpo; un?=que nunca puede coincidir detiene el cuerpo. Las cláusulaselseparten de los hechos anteriores almaybey confrontan los valores fallidos unidos como las cláusulas decase. Los hechos después de untryunen los del final de su cuerpo (sinof) y los de cada cláusula que termina; después de unmaybe, los del final de su cuerpo, los de cada cláusulaelseque termina y, sinelse, los anteriores a él. -
Usos (
semantic/types/inference_uses): una operación que lanza una excepción salvo que un operando tenga un tipo demuestra ese tipo para la variable que leyó, después de que la operación retorne: la aritmética y-/+unarios unnumber(),div,rem, los operadores de bits ybnotuninteger(),and/or/xor,noty el operando izquierdo deandalso/orelseunboolean(),++y--unlist(), un valor llamado un fun de esa aridad, un nombre variable de módulo o de función unatom(), una actualización de mapmap(), un acceso o actualización de record de tupla la tupla del record, un segmento de binary su tipo (<<X:8>>:integer()) y su tamaño unnon_neg_integer(), y una fila por cada comprobación de argumento de un builtin del puente (hd/1,tl/1nonempty_maybe_improper_list(),length/1list(),element/2pos_integer()ytuple(),map_get/2eis_map_key/2map(),atom_to_list/1atom(), ...). La operación conserva su comprobación en tiempo de ejecución. Los nombres vinculados al mismo valor (Y = X, un patrón variable decasesobre una expresión de escrutinio variable) se estrechan juntos. -
Dominios de entrada y de éxito: el dominio de entrada de cada argumento es la unión, sobre las cláusulas de función posibles, de su hecho después de la cabecera y el guard (el hecho estrechado de una variable simple o, si no, el del patrón); su dominio de éxito, la unión, sobre las cláusulas que terminan, de su hecho en su retorno normal.
--print-typesmuestra el dominio de éxito como las entradas (bounded(1..10) -> 1..10,inc(X) -> X + 1comoinc(number()) -> number()), o el dominio de entrada de una función que nunca retorna. Una llamada que retorna estrecha sus argumentos variables al dominio de la función llamada, salvo dentro de una componente recursiva que aún se está resolviendo. -
Los resultados de las cláusulas se unen de forma conservadora: una proyección solo sobrevive si todas las cláusulas devuelven el mismo argumento. Un
caseo unifune los resultados de sus cláusulas de la misma manera. -
Tipos de función (
semantic/types/function_types): además del resumen de unión anterior, una función conserva un tipo de función por cada cláusula posible, como las sobrecargas de un-spec: los hechos de los argumentos después de la cabecera y el guard (el hecho estrechado de una variable simple o, si no, el del patrón) y el resultado de la cláusula (none()para una cláusula que siempre lanza una excepción; las cláusulas imposibles no añaden ninguno). Una cláusula cuyo resultado es un hecho dependiente de sus argumentos (uncaseo unifque la termina, o una variable vinculada a uno, anidado a cualquier profundidad) se divide en un tipo de función por cada cláusula de ese hecho, con los hechos de sus parámetros intersectados con los argumentos que nombran. Los tipos de función con entradas iguales se fusionan (sus resultados se unen); a partir de 32, los últimos se fusionan en uno, con entradas y resultados unidos. Las componentes recursivas los iteran junto con los resultados, y cada ronda une (y después amplía) el resultado de cada tipo; una componente que no converge conserva solo sus resúmenes de unión. El hecho de un fun anónimo conserva un tipo de función por cada cláusula posible (sus patrones y su guard sobre cualquier argumento), yfun F/Alos tipos de función deF/A; los funs que se unen conservan sus tipos de función solo cuando son iguales; si no, se unen como un fun de sus resultados unidos con entradas cualesquiera (intersectarlo con un fun de entradas cualesquiera, como hace el usoF(A), los conserva). La especialización, los dominios y las comprobaciones de especificaciones leen el resumen de unión. -
Las llamadas seleccionan tipos de función: una llamada a una función con tipos de función lee, en orden, cada tipo cuyas entradas se intersectan con todos los hechos de los argumentos, y se detiene tras uno exacto (patrones de variables nuevas usadas una vez, átomos y enteros literales,
[]y tuplas de estos; sin guard o solotruey pruebas de tipo sobre variables de argumento simples; para una rama, un argumento como expresión de escrutinio delcase) cuyas entradas contienen los argumentos: no puede entrarse en ninguna cláusula posterior. Su resultado es la unión de los resultados de los tipos seleccionados, y un resultado igual a un argumento es el hecho de ese argumento dentro del resultado del tipo; si no se selecciona ninguno, la llamada esnone()(solo puede lanzarfunction_clause). Los argumentos desconocidos seleccionan todos los tipos, la unión; una función sin tipos de función (presupuesto, componente no convergida) usa su resumen de unión. Después de que una llamada retorne, sus argumentos variables se estrechan al dominio de éxito y a las entradas unidas de los tipos seleccionados. Las llamadas a valores fun seleccionan de la misma manera los tipos de función del hecho del fun, y un fun anónimo evaluado de nuevo para una llamada (véase Funs) entra en sus cláusulas, las filtra con sus guards y sale de ellas como uncase, así que una cláusula con la que sus argumentos no pueden coincidir no añade nada (F = fun(1) -> one; (_) -> other end, F(2)esother). Las componentes recursivas convergen en resultados y tipos de función. -
Las llamadas evalúan de nuevo la función llamada: cuando los hechos de los argumentos de una llamada están dentro de las entradas de la función llamada y son más estrechos en una de ellas, el cuerpo de la función llamada se evalúa de nuevo con ellos como entradas, como un fun anónimo vinculado, y el resultado de la llamada es el resultado de los tipos de función seleccionados intersectado con el de esa evaluación (
two_callers() -> {add_one(10), add_one(20)}es{11, 21}); los argumentos de la llamada también se estrechan al dominio de éxito de esa evaluación. El resumen, los tipos de función y los hechos de expresiones registrados de la función llamada nunca cambian (la especialización solo lee esos). Presupuestos: 4 evaluaciones anidadas, funciones llamadas de como mucho 256 expresiones, 4.096 unidades de trabajo por llamada de una reserva de 262.144 por pasada sobre el lote (independiente del presupuesto propio del lote); las componentes recursivas nunca se evalúan de nuevo. Superado un presupuesto, la llamada conserva el resultado de los tipos de función seleccionados. -
Un
receivees la unión de sus cláusulas y de su cuerpo deafter, que un tiempo de esperainfinitynunca ejecuta. -
Una componente recursiva empieza el resultado de cada miembro en
none()en su primera resolución, y en las pasadas posteriores parte de los resúmenes con los que terminó su última resolución: en ellas, el primer recorrido de cada miembro sustituye su resumen por lo que encontró el recorrido (el resumen anterior solo sirve como suposición para las llamadas anteriores a ese momento), de modo que nunca quedan tipos de función obsoletos. Vuelve a inferir los miembros hasta que ningún resultado cambia; cada ronda une el resultado nuevo con el anterior (lo amplía tras las primeras 8 rondas, dominio de inferencia), y una llamada recursiva pendiente no añade nada a una unión. Una función que nunca puede retornar sigue siendonone(). Una componente que no ha convergido tras 8 rondas más 4 por miembro, o cuyas rondas han gastado más de una octava parte del presupuesto de la pasada, amplía todos sus miembros aterm()(notificados como ampliados, como al agotar el presupuesto) y una ronda final vuelve a calcular los hechos de expresiones; los hechos de expresiones de las rondas anteriores se descartan, de modo que solo quedan los hechos de las suposiciones finales. Esa componente se recuerda: las pasadas posteriores danterm()a sus miembros de inmediato y ejecutan solo esa ronda final, en lugar de iterar de nuevo hasta el límite. La pasada que amplía todas las entradas aterm()olvida las componentes recordadas, de modo que cada una obtiene una resolución más con esas entradas. -
Las rondas nunca reducen el resumen de un miembro: sus tipos de función se unen tipo a tipo con los de la ronda anterior (los resultados se unen, las entradas cambiadas se unen y el tipo deja de ser exacto) o, cuando el número de tipos ha cambiado, se fusionan los tipos de ambas rondas. Como red de seguridad, cada miembro conserva sus últimos 4 resúmenes, y uno que vuelve a cualquiera de ellos pasa a ser
term()sin tipos de función durante el resto de la resolución, lo que permite converger a los demás miembros. -
Una ronda solo recorre los miembros que leen un resumen cambiado desde su último recorrido: cada recorrido anota los resúmenes que lee (llamadas, resultados de llamadas,
fun F/A), un resumen cambiado marca a sus lectores, y un miembro no marcado conserva sus hechos de expresiones. La primera ronda recorre todos los miembros. Un miembro conserva su último recorrido entre resoluciones: una resolución reutiliza el resultado de ese recorrido, con sus hechos de expresiones y su trabajo, mientras los miembros que leyó tengan los mismos resultados y tipos de función, y todo lo demás que leyó siga igual y reciba las mismas respuestas, como en una componente conservada en las pasadas incrementales. Los resultados son iguales a los de recorrerlo. -
Un presupuesto de trabajo compartido acota la inferencia; el agotamiento pierde precisión y recurre al código genérico, nunca rechaza un programa.
-
Las especificaciones que contradicen la inferencia se diagnostican (
semantic/types/contracts), pero nunca se convierten en hechos, de modo que el programa se comporta en cualquier caso como dice su código. Un tipo declarado se convierte en los hechos que contiene (o más): los tipos integrados por su nombre (byte()es0..255,timeout()esnon_neg_integer() | infinity,iodata()eiolist()listas de cualquier forma), los alias por su definición, los tipos opacos y nominales por su definición dentro de su módulo y como cualquier término fuera de él, los tipos remotos a través del lote, las variables de tipo a través de sus límiteswhen(sin restricciones: cualquier término), y los maps y records por su categoría. Solo un par disjunto es una contradicción, ya que los hechos inferidos pueden contener más valores de los que produce el código:- un dominio de entrada de un argumento que no comparte ningún valor con la spec es un error;
- un resultado inferido que no comparte ninguno (salvo que sea desconocido o
none(), que encaja con cualquier resultado), o una llamada cuyos hechos de argumentos rechazan todas las sobrecargas, es una advertencia (la llamada puede estar en un camino que nunca se ejecuta); none()/no_return()solo admite una función que nunca retorna.
El diagnóstico nombra la función, el tipo declarado tal como está escrito y el inferido. Las especificaciones
-callbackno se comprueban. El compilador de OTP no comprueba las especificaciones (diferencias).
Hechos dependientes
semantic/types/dependent. Un case, if o try ... of se comporta como un
fun de las variables que estrechan sus cláusulas, aplicado a ellas: su valor
es un hecho dependiente, un tipo de función por cada cláusula posible.
- Parámetros: las variables vinculadas antes de la construcción que leen su
expresión de escrutinio (la última expresión del cuerpo de un
try) y sus guards, en el orden del código fuente (las variables que vinculan los patrones de sus cláusulas son nuevas en cada cláusula, no parámetros); como mucho 4, y el estrechamiento de uno posterior se olvida. Cada cláusula posible conserva los hechos de los parámetros después de su patrón y su guard, y su valor (none()para una cláusula que nunca termina); es exacta, como un tipo de función en la selección de llamadas, cuando uncaseo untryconfronta un parámetro o una tupla de parámetros con un patrón y un guard exactos, o uniftiene un guard exacto. - Una expresión de escrutinio que es una tupla de variables estrecha cada
variable a su elemento del valor confrontado, y la variable de un patrón de
tupla se vincula al mismo valor que el elemento de la expresión de
escrutinio en su posición
(
case {X, Y} of {A, b} when is_integer(A) -> ...estrechaXeY). - Un
try ... ofdepende de sus cláusulasof; sus cláusulas catch pueden seguir a cualquier valor de su cuerpo, así que sus valores unidos se unen al valor de cada cláusula (try X of 1 -> one; _ -> other catch _:_ -> error endes$try_of_operator(X :: 1) -> error | one; (X :: _) -> error | other). receiveymaybe ... elsesiguen siendo la unión de sus cláusulas: sus cláusulas confrontan un mensaje o un valor de?=fallido que nada fuera de ellas puede nombrar, así que ninguna lectura podría elegir entre ellas.- Una cláusula cuyo valor es a su vez dependiente (un
caseoifanidado, o una lectura de una variable dependiente) aporta un tipo de función por cada cláusula de ese valor sobre la unión de ambas listas de parámetros, con las entradas intersectadas. Los tipos en los que nunca puede entrarse (una entradanone(), o dentro de las entradas de un tipo exacto anterior) se descartan, y también los parámetros cuyos hechos son los mismos en todos los tipos; las entradas iguales se fusionan y, a partir de 32 tipos, los últimos se fusionan, como en los tipos de función. Una construcción cuyos tipos restantes dan todos el mismo valor, o que no estrecha ningún parámetro, es su unión simple. - Cualquier otro consumidor lee el hecho borrado, la unión de los valores de las cláusulas. Los hechos que salen de la función recorrida (resultados, tipos de función, hechos de funs) se borran; las uniones conservan una dependencia solo cuando ambos lados tienen la misma.
- Una variable vinculada a un valor dependiente lo conserva. Leer la variable
selecciona las cláusulas en las que entran los hechos actuales de sus
parámetros (en el orden de la selección de llamadas, se detiene en una
cláusula exacta cuyas entradas los contienen) y cuyos valores se
intersectan con el hecho estrechado de la propia variable, y el hecho de la
lectura es la unión de sus valores
(
Y = case X of 1 -> one; _ -> other end, case X of 1 -> Y endleeone). - Una variable que vinculan todas las cláusulas de un
caseo unifes la unión de sus hechos al final de las cláusulas que terminan, dependiente de los parámetros de la construcción como su valor:case X of 1 -> Y = 5; _ -> Y = 6 end, Yinfiere(1) -> 5; (_) -> 6. La misma unión se aplica después dereceive,tryymaybe, sin dependencia. - Los operadores, los builtins, las construcciones de tuplas, listas, maps y
records, y las llamadas a funciones del lote (con sus tipos de función
seleccionados como en la selección de llamadas, sin volver a estrechar los
argumentos) con operandos dependientes se evalúan una vez por cada
combinación de las cláusulas de los operandos, con los valores de esas
cláusulas en lugar de los hechos de los operandos y sus entradas
intersectadas: las lecturas de una misma variable toman la misma cláusula,
los demás operandos se combinan, como mucho 16 combinaciones (más allá, el
uso es la unión simple).
R + 1conR = case X of a -> 1; b -> 2 endinfiere(a) -> 2; (b) -> 3. - Estrechar una variable dependiente (un patrón, un guard, un uso) también
estrecha sus parámetros a la unión de las entradas de las cláusulas en las
que se entra cuyos valores se intersectan con el hecho estrechado, y a su vez
los parámetros de estos, hasta 4 niveles de profundidad: después de
R = case X of a -> 1; b -> 2 end,case R of 1 -> X endleeXcomoa. - El resultado dependiente de una cláusula de función la divide (véase Tipos
de función): los parámetros que nombran un argumento, o un nombre vinculado
a su valor, se intersectan con su entrada; los demás se descartan (un tipo
así solo es exacto si su entrada es cualquier término).
nested_case(X, L)concase X of 1 -> case L of spanish -> uno; _ -> one end; _ -> other endinfiere(1, spanish) -> uno; (1, _) -> one; (_, _) -> other. --print-typesanota un valor dependiente como un tipo de función con el nombre de su construcción, cada entrada tras el nombre de su parámetro:case X of 1 -> one; _ -> other endtermina enend. % $case_of_operator(X :: 1) -> one; (X :: _) -> other; unifimprime$if_operator, y untry ... of,$try_of_operator.
Dominio de inferencia
semantic/types/lattice. Un hecho es un conjunto de valores que puede tener
una variable o un resultado. Los hechos se unen donde confluye el flujo de
control (cláusulas, ramas) y se amplían entre las rondas de una componente
recursiva; cada presupuesto de abajo amplía de forma segura a un conjunto
mayor, nunca rechaza un programa. Los hechos se imprimen como tipos de
Erlang, y las categorías por sus nombres integrados.
| Hecho | Impreso | Unión | Presupuesto y ampliación |
|---|---|---|---|
| Nada | none() | Identidad | Una función que nunca retorna sigue siendo none() |
| Cualquier cosa | term() | Absorbe todo hecho | dynamic() y any() son term() |
| Enteros | 42, 1 | 3 | 7 | Unión de singletons | Más de 8 singletons se convierten en su rango |
| Rango de enteros | 1..10, 0..255 | El menor rango que contiene ambos | Un límite que se ha movido entre rondas pasa al siguiente umbral: uno inferior a 1, después 0, después sin límite; uno superior a -1, después sin límite. Los límites dentro de contenedores también se amplían, cuando el hecho anterior tiene una tupla (de ese tamaño y etiqueta), un map (con esas claves) o una lista en el mismo lugar |
| Enteros sin límite | pos_integer() (1 en adelante), non_neg_integer() (0 en adelante), neg_integer() (-1 hacia abajo), integer() | El menor intervalo que contiene ambos, impreso por su categoría | — |
| Flotantes | float() | — | — |
| Números | number() | Un rango o categoría de enteros unido con float() | Los enteros singleton con float() quedan como 1 | float() |
| Átomos | ok, error | ok, boolean() | Unión de singletons; exactamente false y true se imprimen como boolean() | Más de 8 singletons se convierten en atom() |
| Identificadores | pid(), port(), reference() | — | — |
| Tuplas | {ok, 1}, tuple(), #point{x :: 0, y :: _} | Las tuplas del mismo tamaño cuyos primeros elementos no son dos átomos distintos (su etiqueta) se unen elemento a elemento; las demás quedan como miembros separados | Más de 16 elementos se convierten en tuple() salvo que se conozcan todos los elementos; más de 8 formas separadas se convierten en tuple(). Una tupla con el nombre y el tamaño de un record de tupla visible se imprime como el record |
| Listas | [], [T], [T, ...], nonempty_improper_list(H, T), [1, a], [a, b | T] | Los elementos se unen; [] con una lista no vacía da una posiblemente vacía; las listas impropias unen cabezas y colas. Las listas de dos o más elementos conocidos conservan sus posiciones (una notación de Clause, el lenguaje de tipos no tiene ninguna; los elementos se imprimen con | entre paréntesis): las listas posicionales de una misma longitud se unen posición a posición, y si no se unen como listas simples | Una lista de 0..1114111 (char()) se imprime como string() o nonempty_string(); una lista posiblemente vacía de _ se imprime como list() |
| Maps | #{}, #{a := 1}, #{1..17 => a}, map() | Los maps con las mismas claves se unen valor a valor; los maps de otras claves se unen en una asociación de sus claves y valores unidos (=>: cualquier clave puede faltar) | Más de 16 claves se unen en una asociación |
| Funs | fun((term()) -> 1), fun() | Los funs de una misma aridad unen sus resultados; otras aridades dan fun() | — |
| Bitstrings | <<_:16>>, <<_:3, _:_*2>>, binary() | El tamaño menor más cada diferencia de tamaños como unidad | Base y unidad 0/8, 8/8, 0/1, 1/1 se imprimen como binary(), nonempty_binary(), bitstring(), nonempty_bitstring() |
- Una unión conserva un miembro por cada forma unida, en el orden de términos
de Erlang de sus valores: números, átomos,
reference(), funs,port(),pid(), tuplas, maps,[], listas, bitstrings y después los tipos con nombre declarados; más de 8 miembros se convierten enterm(). - Los contenedores se anidan como mucho 4 niveles; un hecho más profundo se
convierte en
term(). - Una unión descarta un miembro tupla exacto que contiene otra tupla exacta de su tamaño (de dos que se contienen mutuamente, queda la anterior), de modo que un conjunto de valores tiene una única unión y las recursiones sobre árboles se estabilizan.
- Las componentes recursivas unen durante 8 rondas (los ciclos de hasta 8 funciones convergen exactamente) y después amplían (inferencia).
- El estrechamiento (
Lattice::meet) es la intersección de los hechos, los valores que contienen ambos (o más, peronone()solo cuando no comparten ninguno). Una intersección vacía significa que el camino no puede ejecutarse. - Los hechos son nodos de un grafo que nunca cambian, por lo que el grafo
conserva la respuesta de cada unión de dos hechos, intersección y prueba de
inclusión (
within) durante el resto del lote: casi todas se repiten. Una respuesta conservada sigue contando las uniones que ejecutó para el abandono según el coste, de modo que los resultados no dependen de ella; no se conserva una respuesta calculada mientras el presupuesto de nodos ampliaba un hecho.
En -O2 con especialización por tipos, el lowering usa estos hechos para
eliminar comprobaciones (especialización); las
especificaciones nunca se convierten en hechos.
Caché de inferencia
Los módulos de los directorios de la
caché de inferencia pueden empezar una
compilación con los resultados que conservó una compilación anterior:
resúmenes, entradas locales y hechos de expresiones de sus funciones. infer
los mantiene fijos en cada pasada, nunca recorre una componente cuyos miembros
los reutilizan todos, y recorre cualquier otra función como de costumbre,
leyendo los resúmenes conservados como cualquier otro.
- De qué dependen los hechos de un módulo. Las funciones exportadas y las
funciones que nombra un
fun F/Aparten determ(); las entradas de cualquier otra función proceden de sus llamadores, que están en el mismo módulo; un hecho nombra una función de otro módulo solo a través de un átomo: una llamada remota,fun M:F/A, oapply/2,3sobre un hecho de átomo (y lee la disposición de records de otro módulo solo a través de un record nativo que cualifica o importa), y todo hecho de átomo procede de un átomo que el módulo escribe o de un hecho de un módulo que alcanza. Así, los hechos de un módulo dependen solo de su propio código y del código de los módulos del lote que nombran sus átomos, de forma transitiva. La clave de sus resultados conservados cubre exactamente eso: la identidad declau, la huella del módulo (su código fuente, cada archivo que incluye, las opciones de preprocesamiento y la respuesta de un parse transform), las formas predefinidas, el estado de escript y de biblioteca, y lo mismo de cada módulo del lote que alcanzan sus átomos. Un módulo alcanzado de esa manera que no tiene huella hace que el módulo no pueda almacenarse en caché. - Vivacidad. La vivacidad de las exportaciones solo elimina funciones, y
un llamador eliminado solo estrecha las entradas de las funciones a las que
llamaba: los hechos calculados con más funciones vivas siguen siendo
válidos. Los resultados conservados se reutilizan cuando contienen todas
las funciones vivas ahora (
results do not fiten otro caso); las funciones omitidas no reciben nada. - Mezcla. Los resultados conservados y los nuevos pueden encontrarse en un mismo lote: los hechos de cada módulo son válidos para cualquier ejecución del programa, digan lo que digan los hechos de otro módulo, así que un módulo nuevo que lee un resumen conservado (o al revés) sigue siendo correcto. Una componente recursiva con miembros de ambos tipos se recorre entera.
- Precisión. Los resultados reutilizados son los de la compilación que los conservó. Los resultados de un módulo nuevo pueden seguir difiriendo de los de una compilación sin la caché: el presupuesto de la pasada ya no paga por los módulos reutilizados, y las identidades del grafo, por las que se ordenan algunos miembros de unión, se numeran de otra manera.
--print-types
Imprime cada módulo del lote (en orden de entrada/destino, con los módulos de biblioteca después) como código fuente Erlang (impresión del código fuente) con lo que encontró la inferencia de tipos. La salida va a stdout y es legible por humanos, no es Erlang ni un formato de intercambio. Las advertencias siguen yendo a stderr.
%% module "branches" source="branches.erl" target="" declared=complete inferred=complete
-module(branches).
-export([mixed/1]).
%% inferred: mixed(1) -> 1; (_) -> 2
mixed(X) ->
case X of
1 ->
1;
_ ->
2
end. % $case_of_operator(X :: 1) -> 1; (X :: _) -> 2
- Una línea
%% modulenombra el módulo, su código fuente, el destino del proyecto y si los tipos declarados e inferidos se completaron o un límite los amplió. - Las declaraciones (
-type,-spec,-callback, records) aparecen tal como están escritas. - Encima de cada función,
%% declared: f(Inputs) -> Resultda cada sobrecarga de su-spectal como se resolvió (con sus restriccioneswhen), y%% inferred: f(Inputs) -> Resultlo que encontró la inferencia, para poder comparar ambos. Las entradas son el dominio de éxito de cada argumento (a partir determ()para las funciones exportadas y las que nombra unfun F/A, a partir de los argumentos unidos de los llamadores para las demás funciones). Una función con varios tipos de función imprime una firma por tipo, cada una con sus propias entradas:f(integer()) -> integer(); (atom()) -> string()(semantic::types::function_source). Un-specse mantiene con la función justo después de él, separado de otras formas por una línea en blanco. - La expresión más externa de cada línea (una expresión del cuerpo, o la
expresión de escrutinio de un
case) cuyo hecho dice más queterm()se anota con un comentario final% Typedespués de la puntuación de la línea (un tipo conocido oculta una relación con un argumento, que solo se muestra para un valor del que no se sabe nada más): como mucho una nota por línea, ninguna para las expresiones anidadas en ella, de modo que la salida sigue siendo Erlang válido. Una expresión del cuerpo que ocupa varias líneas lleva su nota en la última (end, % ...). Los términos literales (literales, y tuplas, listas, maps construidos y bitstrings de literales) no se anotan; una coincidencia muestra su valor. Un valor dependiente se imprime como un tipo de función de su construcción (hechos dependientes). - Un valor que la inferencia ha demostrado igual a uno de los argumentos de la
función, y del que no se sabe nada más, se imprime como el nombre de ese
argumento, una variable de tipo: la variable que le da la primera cláusula
que vincula el argumento entero o, si no,
_argumentN(empezando en 1, también cuando un argumento anterior tomó el nombre). La entrada del argumento muestra el mismo nombre cuando es cualquier término:second(_, Y) -> Y,keep(Acc, number()) -> Acc. Una variable muestra solo su tipo; su nombre ya indica qué argumento es.
Expectativas de inferencia
tests/fixtures/inference/*.erl registran lo que la inferencia debería
encontrar para cada función y lo que encuentra hoy. Cada módulo se convierte
en la prueba de CTest inference_<module>
(tests/compiler/inference/expectations.py):
%% expect: sum() -> 3
%% today: sum() -> _
sum() -> 1 + 2.
expect:es la firma que--print-typesdebería imprimir en su línea%% inferred:;today:, presente mientras la inferencia se queda corta, es la que imprime ahora.- La comprobación compara la salida con
todaycuando existe y, si no, conexpect; cada función del módulo necesita una líneaexpect. Una líneatodaya la que la inferencia ya ha dado alcance hace fallar la comprobación hasta que se elimina. expectations.py <clau> <fixture> --recordreescribe las líneastodaya partir de la salida actual (una acción de mantenimiento: revisar el diff).values.erlcubre valores literales y construidos, resultados de operadores y builtins, contenedores y sus partes, funs y sus llamadas, entradas locales procedentes de los llamadores, uniones y rangos de enteros, relaciones con argumentos y valores detry/maybe;narrowing.erl, pruebas de tipo, guards, patrones, rangos de comparación, disyunciones y estrechamientos que no deben filtrarse;base_types.erl, una función por cada tipo base e integrado del lenguaje de tipos, cuyo-specdebe resolverse y cuyo cuerpo produce un valor así;clauses.erl, tipos de función, selección de llamadas y evaluación por llamada dentro y más allá de sus presupuestos;dependent.erl, hechos dependientes;trees.erlyparsers.erl, recursiones sobre árboles y formas de analizadores sintácticos generados;widening.erl, contadores dentro de contenedores que se amplían y se estabilizan.tests/fixtures/inference/contracts/contiene las comprobaciones de especificaciones (prueba de CTestinference_contracts).tests/fixtures/inference/budgets/walkers.erles una pasada del compilador con la forma habitual de Erlang (un record de estado grande que se pasa entre recorridos mutuamente recursivos cuyos resultados son uniones de muchos records). La prueba de CTestinference_walkers(tests/compiler/inference/walkers.py) mantiene sus pasadas, pasos de trabajo, uniones del retículo y las resoluciones, rondas y recorridos de miembros del grupo dentro de presupuestos aproximadamente un cuarto superiores a los recuentos actuales, y ejecuta el programa.
Impresión de tipos
semantic::types::type_source(graph, type) (semantic/types/printing)
representa un tipo del grafo de tipos en la sintaxis de tipos de Erlang: _
para cualquier término (term(), escrito mediante TERM_SOURCE por
brevedad; la sintaxis de tipos lee _ como any()), none(), átomos y
enteros, 1..5, {ok, T}, tuple(), [T], [T, ...],
#{K => V, K := V}, #r{f :: T}, <<_:B, _:_*U>>, fun((A) -> R),
A | B. Un fun de varios tipos de función los imprime en una notación de
Clause, fun((1) -> one; (_) -> other): la sintaxis de tipos de Erlang no tiene un tipo fun
sobrecargado, y una unión de tipos fun significa otra cosa. Los enteros de una
unión se imprimen en orden de valor donde está su primer entero, y los
consecutivos como un rango (1 | 2 | 3 | 5 se imprime como 1..3 | 5; el
hecho conserva los singletons). Los tipos predefinidos de erlang omiten su
módulo; las referencias a tipos declarados conservan su nombre. Un
presupuesto de nodos acota el texto; más allá de él, y a más de 32 niveles de
anidamiento, aparece ... en su lugar.
clau --print-types answer.erl client.erl
clau --print-types --project project.toml --target demo --verbose
Clause