Uma função ou macro pode especificar avisos de compilador de bytes?

15

Estou escrevendo uma função que, em princípio, recebe um número arbitrário de argumentos. Na prática, no entanto, só deve ser passado um número par de argumentos e, caso contrário, produzirá resultados indesejáveis.

Aqui está um exemplo fictício para o contexto:

(defun my-caller (&rest args)
  (while args
    (call-other-function (pop args) (pop args))))

Quando um arquivo elisp é compilado em bytes, o compilador de bytes lança um aviso quando vê uma função sendo chamada com o número errado de argumentos. Obviamente, isso nunca vai acontecer my-caller, pois está definido para aceitar qualquer número.

Ainda assim, talvez haja uma propriedade de símbolo que eu possa definir ou um (declare)formulário que eu possa adicionar à sua definição. Algo para notificar o usuário de que essa função deve receber apenas um número par de argumentos.

  1. Existe uma maneira de informar o compilador de bytes sobre essa restrição?
  2. Caso contrário, é possível com uma macro, em vez de uma função?
Malabarba
fonte
"... quando vê uma função sendo invocada com o número errado de argumentos"?
itsjeyd

Respostas:

13

EDIT : Uma maneira melhor de fazer isso no Emacs recente é definindo uma macro do compilador para verificar o número de argumentos. Minha resposta original usando uma macro normal é preservada abaixo, mas uma macro de compilador é superior porque não impede a passagem da função para funcallou applyem tempo de execução.

Nas versões recentes do Emacs, você pode fazer isso definindo uma macro de compilador para sua função que verifica o número de argumentos e produz um aviso (ou até mesmo um erro) se não corresponder. A única sutileza é que a macro do compilador deve retornar o formulário de chamada de função original inalterado para avaliação ou compilação. Isso é feito usando um &wholeargumento e retornando seu valor. Isso poderia ser realizado assim:

(require 'cl-lib)

(defun my-caller (&rest args)
  (while args
    (message "%S %S" (pop args) (pop args))))

(define-compiler-macro my-caller (&whole form &rest args)
  (when (not (cl-evenp (length args)))
    (byte-compile-warn "`my-caller' requires an even number of arguments"))
  form)

(my-caller 1 2 3 4)
(my-caller 1 2)
(funcall #'my-caller 1 2 3 4)       ; ok
(apply #'my-caller '(1 2))          ; also ok
(my-caller 1)                       ; produces a warning
(funcall #'my-caller 1 2 3)         ; no warning!
(apply #'my-caller '(1 2 3))        ; also no warning

Note-se que funcalle applyagora pode ser usado, mas eles Ignorar verificação argumento pela macro compilador. Apesar do seu nome, macros compilador também parecem ser expandida no curso de 'interpretada' avaliação via C-xC-e, M-xeval-buffer, assim que você vai obter erros na avaliação, bem como sobre a compilação neste exemplo.


A resposta original segue:

Aqui está como você pode implementar a sugestão de Jordon de "usar uma macro que fornecerá avisos no momento da expansão". Acontece que é muito fácil:

(require 'cl-lib)

(defmacro my-caller (&rest args)
  (if (cl-evenp (length args))
      `(my-caller--function ,@args)
    (error "Function `my-caller' requires an even number of arguments")))

(defun my-caller--function (&rest args)
  ;; function body goes here
  args)

(my-caller 1 2 3 4)
(my-caller 1 2 3)

A tentativa de compilar o acima em um arquivo falhará (nenhum .elcarquivo é produzido), com uma boa mensagem de erro clicável no log de compilação, informando:

test.el:14:1:Error: `my-caller' requires an even number of arguments

Você também pode substituir (error …)por (byte-compile-warn …)para produzir um aviso em vez de um erro, permitindo que a compilação continue. (Obrigado a Jordon por apontar isso nos comentários).

Como as macros são expandidas no momento da compilação, não há penalidade no tempo de execução associada a essa verificação. Obviamente, você não pode impedir que outras pessoas liguem my-caller--functiondiretamente, mas pelo menos pode anunciar como uma função "privada" usando a convenção de hífen duplo.

Uma desvantagem notável de usar uma macro para esse fim é que ela my-callernão é mais uma função de primeira classe: você não pode passá-la para funcallou applyem tempo de execução (ou pelo menos ela não fará o que você espera). Nesse aspecto, essa solução não é tão boa quanto poder simplesmente declarar um aviso do compilador para uma função real. Obviamente, o uso applytornaria impossível verificar o número de argumentos que estão sendo passados ​​para a função em tempo de compilação, portanto, talvez essa seja uma troca aceitável.

Jon O.
fonte
2
Os avisos de compilação são criados combyte-compile-warn
Jordon Biondo 20/10
Agora me pergunto se isso poderia ser realizado de maneira mais eficaz, definindo uma macro de compilador para a função. Isso eliminaria a desvantagem de não estar no applyou funcallno invólucro da macro. Vou testá-lo e editar minha resposta, se funcionar.
Jon O.
11

Sim, você pode usar byte-defop-compilerpara realmente especificar uma função que compila sua função, byte-defop-compilerpossui alguns detalhes internos para ajudá-lo a especificar que suas funções devem gerar avisos com base em vários argumentos.

Documentação

Adicione um formulário do compilador para FUNCTION.Se a função for um símbolo, a variável "byte-SYMBOL" deve nomear o código de operação a ser usado. Se a função for uma lista, o primeiro elemento é a função e o segundo elemento é o símbolo do bytecode. O segundo elemento pode ser nulo, significando que não há código de operação. COMPILE-HANDLER é a função a ser usada para compilar esse byte-op ou pode ser a abreviatura 0, 1, 2, 3, 0-1 ou 1-2. Se for nulo, o manipulador é "byte-compile-SYMBOL.


Uso

No seu caso específico, você pode usar uma das abreviações para definir que sua função receba dois argumentos.

(byte-defop-compiler my-caller 2)

Agora sua função emitirá avisos quando compilada com qualquer coisa, exceto 2 args.

Se você deseja dar avisos mais específicos e escrever suas próprias funções de compilador. Veja byte-compile-one-arge outras funções semelhantes no bytecomp.el para referência.

Observe que você não está apenas especificando alguma função para lidar com a validação, mas também a compilação. Novamente, as funções de compilação no bytecomp.el fornecerão uma boa referência.


Rotas mais seguras

Isso não é algo que eu tenha visto documentado ou discutido on-line, mas no geral eu diria que esse é um caminho inadequado. A rota correta (IMO) seria escrever seus defun com assinaturas descritivas ou usar uma macro que fornecerá avisos no momento da expansão, verificando o comprimento de seus argumentos e usando byte-compile-warnou errorpara mostrar erros. Também pode ser útil eval-when-compilefazer a verificação de erros.

Você também precisará que sua função seja definida antes de ser usada, e a chamada byte-defop-compilerprecisará ser feita antes que o compilador atenda às chamadas reais de sua função.

Novamente, parece não estar realmente documentado ou aconselhado pelo que vi (pode estar errado), mas imagino que o padrão a seguir aqui seja especificar algum tipo de arquivo de cabeçalho para o seu pacote que esteja cheio de desonestos vazios e liga para byte-defop-compiler. Basicamente, este seria um pacote necessário para que seu pacote real possa ser compilado.

Opinião: Com base no que sei, o que não é muito, porque acabei de aprender sobre tudo isso, aconselho você a nunca fazer nada disso. sempre

Jordon Biondo
fonte
11
Relacionado: Há bytecomp-simplify, que ensina avisos adicionais ao compilador de bytes.
Wilfred Hughes