diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/hipe/icode/hipe_beam_to_icode.erl | 28 | ||||
-rw-r--r-- | lib/hipe/icode/hipe_icode_primops.erl | 8 | ||||
-rw-r--r-- | lib/hipe/rtl/hipe_rtl_binary.erl | 2 | ||||
-rw-r--r-- | lib/hipe/rtl/hipe_rtl_binary_match.erl | 37 | ||||
-rw-r--r-- | lib/hipe/test/bs_SUITE_data/bs_pmatch_bugs.erl | 48 | ||||
-rw-r--r-- | lib/tools/emacs/Makefile | 1 | ||||
-rw-r--r-- | lib/tools/emacs/erlang-start.el | 14 | ||||
-rw-r--r-- | lib/tools/emacs/erlang.el | 5 | ||||
-rw-r--r-- | lib/tools/emacs/erldoc.el | 508 |
9 files changed, 602 insertions, 49 deletions
diff --git a/lib/hipe/icode/hipe_beam_to_icode.erl b/lib/hipe/icode/hipe_beam_to_icode.erl index 224aacd8d7..3386523206 100644 --- a/lib/hipe/icode/hipe_beam_to_icode.erl +++ b/lib/hipe/icode/hipe_beam_to_icode.erl @@ -763,32 +763,10 @@ trans_fun([{test,bs_test_unit,{f,Lbl},[Ms,Unit]}| [MsVar], [], Env, Instructions); trans_fun([{test,bs_match_string,{f,Lbl},[Ms,BitSize,Bin]}| Instructions], Env) -> - True = mk_label(new), - FalseLabName = map_label(Lbl), - TrueLabName = hipe_icode:label_name(True), + %% the current match buffer MsVar = mk_var(Ms), - TmpVar = mk_var(new), - ByteSize = BitSize div 8, - ExtraBits = BitSize rem 8, - WordSize = hipe_rtl_arch:word_size(), - if ExtraBits =:= 0 -> - trans_op_call({hipe_bs_primop,{bs_match_string,Bin,ByteSize}}, Lbl, - [MsVar], [MsVar], Env, Instructions); - BitSize =< ((WordSize * 8) - 5) -> - <<Int:BitSize, _/bits>> = Bin, - {I1,Env1} = trans_one_op_call({hipe_bs_primop,{bs_get_integer,BitSize,0}}, Lbl, - [MsVar], [TmpVar, MsVar], Env), - I2 = hipe_icode:mk_type([TmpVar], {integer,Int}, TrueLabName, FalseLabName), - I1 ++ [I2,True] ++ trans_fun(Instructions, Env1); - true -> - <<RealBin:ByteSize/binary, Int:ExtraBits, _/bits>> = Bin, - {I1,Env1} = trans_one_op_call({hipe_bs_primop,{bs_match_string,RealBin,ByteSize}}, Lbl, - [MsVar], [MsVar], Env), - {I2,Env2} = trans_one_op_call({hipe_bs_primop,{bs_get_integer,ExtraBits,0}}, Lbl, - [MsVar], [TmpVar, MsVar], Env1), - I3 = hipe_icode:mk_type([TmpVar], {integer,Int}, TrueLabName, FalseLabName), - I1 ++ I2 ++ [I3,True] ++ trans_fun(Instructions, Env2) - end; + Primop = {hipe_bs_primop, {bs_match_string, Bin, BitSize}}, + trans_op_call(Primop, Lbl, [MsVar], [MsVar], Env, Instructions); trans_fun([{bs_context_to_binary,Var}|Instructions], Env) -> %% the current match buffer IVars = [trans_arg(Var)], diff --git a/lib/hipe/icode/hipe_icode_primops.erl b/lib/hipe/icode/hipe_icode_primops.erl index cee37b6a57..2a141c514e 100644 --- a/lib/hipe/icode/hipe_icode_primops.erl +++ b/lib/hipe/icode/hipe_icode_primops.erl @@ -287,8 +287,8 @@ pp(Dev, Op) -> io:format(Dev, "bs_start_match<~w>", [Max]); {{bs_start_match, Type}, Max} -> io:format(Dev, "bs_start_match<~w,~w>", [Type,Max]); - {bs_match_string, String, SizeInBytes} -> - io:format(Dev, "bs_match_string<~w, ~w>", [String, SizeInBytes]); + {bs_match_string, String, SizeInBits} -> + io:format(Dev, "bs_match_string<~w, ~w>", [String, SizeInBits]); {bs_get_integer, Size, Flags} -> io:format(Dev, "bs_get_integer<~w, ~w>", [Size, Flags]); {bs_get_float, Size, Flags} -> @@ -596,10 +596,10 @@ type(Primop, Args) -> erl_types:t_subtract(Type, erl_types:t_matchstate()), erl_types:t_matchstate_slot( erl_types:t_inf(Type, erl_types:t_matchstate()), 0)); - {hipe_bs_primop, {bs_match_string,_,Bytes}} -> + {hipe_bs_primop, {bs_match_string,_,Bits}} -> [MatchState] = Args, BinType = erl_types:t_matchstate_present(MatchState), - NewBinType = match_bin(erl_types:t_bitstr(0, Bytes*8), BinType), + NewBinType = match_bin(erl_types:t_bitstr(0, Bits), BinType), erl_types:t_matchstate_update_present(NewBinType, MatchState); {hipe_bs_primop, {bs_test_unit,Unit}} -> [MatchState] = Args, diff --git a/lib/hipe/rtl/hipe_rtl_binary.erl b/lib/hipe/rtl/hipe_rtl_binary.erl index fb9c0c196d..9b400f4c93 100644 --- a/lib/hipe/rtl/hipe_rtl_binary.erl +++ b/lib/hipe/rtl/hipe_rtl_binary.erl @@ -19,7 +19,7 @@ %%% %CopyrightEnd% %%% %%%------------------------------------------------------------------- -%%% File : hipe_rtl_binary_2.erl +%%% File : hipe_rtl_binary.erl %%% Author : Per Gustafsson <[email protected]> %%% Description : %%% diff --git a/lib/hipe/rtl/hipe_rtl_binary_match.erl b/lib/hipe/rtl/hipe_rtl_binary_match.erl index 528672b893..d999cd2743 100644 --- a/lib/hipe/rtl/hipe_rtl_binary_match.erl +++ b/lib/hipe/rtl/hipe_rtl_binary_match.erl @@ -270,24 +270,23 @@ gen_rtl({bs_save, Slot}, [NewMs], [Ms], TrueLblName, _FalseLblName) -> set_field_from_term({matchstate, {saveoffset, Slot}}, Ms, Offset), hipe_rtl:mk_goto(TrueLblName)]; %% ----- bs_match_string ----- -gen_rtl({bs_match_string, String, ByteSize}, Dst, [Ms], +gen_rtl({bs_match_string, String, BitSize}, Dst, [Ms], TrueLblName, FalseLblName) -> {[Offset, BinSize, Base], Instrs} = extract_matchstate_vars([offset, binsize, base], Ms), [SuccessLbl, ALbl, ULbl] = create_lbls(3), [NewOffset, BitOffset] = create_gcsafe_regs(2), - Unit = hipe_rtl_arch:word_size() - 1, - Loops = ByteSize div Unit, - Init = + Unit = (hipe_rtl_arch:word_size() - 1) * ?BYTE_SIZE, + Init = [Instrs, opt_update_ms(Dst, Ms), - check_size(Offset, hipe_rtl:mk_imm(ByteSize*?BYTE_SIZE), BinSize, + check_size(Offset, hipe_rtl:mk_imm(BitSize), BinSize, NewOffset, hipe_rtl:label_name(SuccessLbl), FalseLblName), SuccessLbl], SplitCode = [hipe_rtl:mk_alub(BitOffset, Offset, 'and', hipe_rtl:mk_imm(?LOW_BITS), eq, hipe_rtl:label_name(ALbl), hipe_rtl:label_name(ULbl))], - Loops = ByteSize div Unit, + Loops = BitSize div Unit, SkipSize = Loops * Unit, {ACode1, UCode1} = case Loops of @@ -297,9 +296,9 @@ gen_rtl({bs_match_string, String, ByteSize}, Dst, [Ms], create_loops(Loops, Unit, String, Base, Offset, BitOffset, FalseLblName) end, - <<_:SkipSize/binary, RestString/binary>> = String, + <<_:SkipSize/bits, RestString/bits>> = String, {ACode2, UCode2} = - case ByteSize rem Unit of + case BitSize rem Unit of 0 -> {[], []}; Rem -> @@ -393,12 +392,12 @@ validate_unicode_retract_c_code(Src, Ms, TrueLblName, FalseLblName) -> create_loops(Loops, Unit, String, Base, Offset, BitOffset, FalseLblName) -> [Reg] = create_gcsafe_regs(1), AlignedFun = fun(Value) -> - [get_int_to_reg(Reg, Unit*?BYTE_SIZE, Base, Offset, 'srl', + [get_int_to_reg(Reg, Unit, Base, Offset, 'srl', {unsigned, big}), update_and_test(Reg, Unit, Offset, Value, FalseLblName)] end, UnAlignedFun = fun(Value) -> - [get_unaligned_int_to_reg(Reg, Unit*?BYTE_SIZE, + [get_unaligned_int_to_reg(Reg, Unit, Base, Offset, BitOffset, 'srl', {unsigned, big})| update_and_test(Reg, Unit, Offset, Value, FalseLblName)] @@ -406,31 +405,31 @@ create_loops(Loops, Unit, String, Base, Offset, BitOffset, FalseLblName) -> {create_loops(Loops, Unit, String, AlignedFun), create_loops(Loops, Unit, String, UnAlignedFun)}. -create_rests(Rem, String, Base, Offset, BitOffset, FalseLblName) -> +create_rests(RemBits, String, Base, Offset, BitOffset, FalseLblName) -> [Reg] = create_gcsafe_regs(1), AlignedFun = fun(Value) -> - [get_int_to_reg(Reg, Rem*?BYTE_SIZE, Base, Offset, 'srl', + [get_int_to_reg(Reg, RemBits, Base, Offset, 'srl', {unsigned, big})| just_test(Reg, Value, FalseLblName)] end, UnAlignedFun = fun(Value) -> - [get_unaligned_int_to_reg(Reg, Rem*?BYTE_SIZE, + [get_unaligned_int_to_reg(Reg, RemBits, Base, Offset, BitOffset, 'srl', {unsigned, big})| just_test(Reg, Value, FalseLblName)] end, - {create_loops(1, Rem, String, AlignedFun), - create_loops(1, Rem, String, UnAlignedFun)}. + {create_loops(1, RemBits, String, AlignedFun), + create_loops(1, RemBits, String, UnAlignedFun)}. create_loops(0, _Unit, _String, _IntFun) -> []; create_loops(N, Unit, String, IntFun) -> - {Value, RestString} = get_value(Unit,String), + {Value, RestString} = get_value(Unit, String), [IntFun(Value), create_loops(N-1, Unit, RestString, IntFun)]. update_and_test(Reg, Unit, Offset, Value, FalseLblName) -> - [add_to_offset(Offset, Offset, hipe_rtl:mk_imm(Unit*?BYTE_SIZE), FalseLblName), + [add_to_offset(Offset, Offset, hipe_rtl:mk_imm(Unit), FalseLblName), just_test(Reg, Value, FalseLblName)]. just_test(Reg, Value, FalseLblName) -> @@ -439,8 +438,8 @@ just_test(Reg, Value, FalseLblName) -> hipe_rtl:label_name(ContLbl), FalseLblName), ContLbl]. -get_value(N,String) -> - <<I:N/integer-unit:8, Rest/binary>> = String, +get_value(N, String) -> + <<I:N, Rest/bits>> = String, {I, Rest}. make_int_gc_code(I) when is_integer(I) -> diff --git a/lib/hipe/test/bs_SUITE_data/bs_pmatch_bugs.erl b/lib/hipe/test/bs_SUITE_data/bs_pmatch_bugs.erl index b280705a47..d9f3278b45 100644 --- a/lib/hipe/test/bs_SUITE_data/bs_pmatch_bugs.erl +++ b/lib/hipe/test/bs_SUITE_data/bs_pmatch_bugs.erl @@ -9,6 +9,7 @@ test() -> <<49,50,51>> = lex_digits1(Bin, 1, []), <<49,50,51>> = lex_digits2(Bin, 1, []), ok = var_bind_bug(<<1, 2, 3, 4, 5, 6, 7, 8>>), + ok = bs_match_string_bug(), ok. %%-------------------------------------------------------------------- @@ -65,3 +66,50 @@ var_bind_bug(<<A:1/binary, B:8/integer, _C:B/binary, _Rest/binary>>) -> B -> wrong; _ -> ok end. + +%%-------------------------------------------------------------------- +%% From: Andreas Schultz +%% Date: 2/11/2016 +%% +%% Either HiPE is messing up binary matches in some cases or I'm not +%% seeing the problem. ... <SNIP PROGRAM - CLEANED UP VERSION BELOW> +%% With Erlang 19.1.3 the HiPE compiled version behaves differently +%% than the non-HiPE version: ... <SNIP TEST RUNS> +%% So, do I do something wrong here or is this a legitimate HiPE bug? +%% +%% Yes, this was a legitimate HiPE bug: The BEAM to ICode tranaslation +%% of the bs_match_string instruction, written long ago for binaries +%% (i.e., with byte-sized strings), tried to do a `clever' translation +%% of even bit-sized strings using a HiPE primop that took a `Size' +%% argument expressed in *bytes*. ICode is not really the place to do +%% such a thing, and moreover there is really no reason for the HiPE +%% primop not to take a Size argument expressed in *bits* instead. +%% The bug was fixed by changing the `Size' argument to be in bits, +%% postponing the translation of the bs_match_string primop until RTL +%% and doing a proper translation using bit-sized quantities there. +%%-------------------------------------------------------------------- + +bs_match_string_bug() -> + ok = test0(<<50>>), + Bin = data(), + ok = test1(Bin), + ok = test2(Bin), + ok. + +%% Minimal test case showing the problem matching with strings +test0(<<6:5, 0:1, 0:2>>) -> weird; +test0(<<6:5, _:1, _:2>>) -> ok; +test0(_) -> default. + +data() -> <<50,16,0>>. + +%% This was the problematic test case in HiPE: 'default' was returned +test1(<<1:3, 1:1, _:1, 0:1, 0:1, 0:1, _/binary>>) -> weird; +test1(<<1:3, 1:1, _:1, _:1, _:1, _:1, _/binary>>) -> ok; +test1(_) -> default. + +%% This variation of test1/1 above worked OK, even in HiPE +test2(<<1:3, 1:1, _:1, A:1, B:1, C:1, _/binary>>) + when A =:= 1; B =:= 1; C =:= 1 -> ok; +test2(<<1:3, 1:1, _:1, 0:1, 0:1, 0:1, _/binary>>) -> weird; +test2(_) -> default. diff --git a/lib/tools/emacs/Makefile b/lib/tools/emacs/Makefile index e1b195ef97..35c93ba4ed 100644 --- a/lib/tools/emacs/Makefile +++ b/lib/tools/emacs/Makefile @@ -38,6 +38,7 @@ MAN_FILES= \ tags.3 EMACS_FILES= \ + erldoc \ erlang-skels \ erlang-skels-old \ erlang_appwiz \ diff --git a/lib/tools/emacs/erlang-start.el b/lib/tools/emacs/erlang-start.el index f9a6d24b2c..160057e179 100644 --- a/lib/tools/emacs/erlang-start.el +++ b/lib/tools/emacs/erlang-start.el @@ -78,9 +78,23 @@ (autoload 'erlang-find-tag-other-window "erlang" "Like `find-tag-other-window'. Capable of retreiving Erlang modules.") +;; +;; Declare functions in "erlang-edoc.el". +;; + (autoload 'erlang-edoc-mode "erlang-edoc" "Toggle Erlang-Edoc mode on or off." t) ;; +;; Declare functions in "erldoc.el". +;; + +(autoload 'erldoc-browse "erldoc" "\n\n(fn MFA)" t nil) +(autoload 'erldoc-browse-topic "erldoc" "\n\n(fn TOPIC)" t nil) +(autoload 'erldoc-apropos "erldoc" "\n\n(fn PATTERN)" t nil) +(autoload 'erldoc-eldoc-function "erldoc" "\ +A function suitable for `eldoc-documentation-function'.\n\n(fn)" nil nil) + +;; ;; Associate files extensions ".erl" and ".hrl" with Erlang mode. ;; diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index cc22903e7f..40f0bb7f80 100644 --- a/lib/tools/emacs/erlang.el +++ b/lib/tools/emacs/erlang.el @@ -1440,6 +1440,11 @@ Other commands: (erlang-skel-init) (when (fboundp 'tempo-use-tag-list) (tempo-use-tag-list 'erlang-tempo-tags)) + (when (and (fboundp 'add-function) (fboundp 'erldoc-eldoc-function)) + (or eldoc-documentation-function + (setq-local eldoc-documentation-function #'ignore)) + (add-function :before-until (local 'eldoc-documentation-function) + #'erldoc-eldoc-function)) (run-hooks 'erlang-mode-hook) (if (zerop (buffer-size)) (run-hooks 'erlang-new-file-hook))) diff --git a/lib/tools/emacs/erldoc.el b/lib/tools/emacs/erldoc.el new file mode 100644 index 0000000000..cb355374d9 --- /dev/null +++ b/lib/tools/emacs/erldoc.el @@ -0,0 +1,508 @@ +;;; erldoc.el --- browse Erlang/OTP documentation -*- lexical-binding: t; -*- + +;; %CopyrightBegin% +;; +;; Copyright Ericsson AB 2016. All Rights Reserved. +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; You may obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. +;; +;; %CopyrightEnd% + +;;; Commentary: + +;; Crawl Erlang/OTP HTML documentation and generate lookup tables. +;; +;; This package depends on `cl-lib', `pcase' and +;; `libxml-parse-html-region'; emacs 24+ compiled with libxml2 should +;; work. On emacs 24.1 and 24.2 do `M-x package-install RET cl-lib +;; RET' to install `cl-lib'. +;; +;; Please customise `erldoc-man-index' to point to your local OTP +;; documentation. +;; +;; To use: +;; +;; (define-key help-map "u" 'erldoc-browse) +;; (define-key help-map "t" 'erldoc-browse-topic) +;; (define-key help-map "a" 'erldoc-apropos) +;; +;; Note: these commands trigger indexing OTP documentation on first +;; run with cache to disk which may take 1-2 minutes. + + +;;; Examples: + +;; 1. `M-x erldoc-browse RET erlang:integer_to_binary/2 RET' opens the +;; `erlang' manual anchored on the entry for `integer_to_binary/2'. +;; +;; 2. `M-x erldoc-apropos RET first RET' list all MFAs matching +;; substring `first'. +;; +;; 3. `M-x erldoc-browse-topic RET efficiency_guide#Introduction RET' +;; opens chapter `Introduction' of the `Efficiency Guide' in the +;; browser. + +;;; History: + +;; Written in December 2013 as a temporary solution to help me browse +;; the rich Erlang/OTP documentation. Three years on I find myself +;; still using it every day. - Leo (2016) + +;;; Code: + +(eval-when-compile (require 'url-parse)) +(require 'cl-lib) +(require 'erlang) + +(eval-and-compile ;for emacs < 24.3 + (or (fboundp 'user-error) (defalias 'user-error 'error))) + +(defgroup erldoc nil + "Browse Erlang document." + :group 'help) + +(defcustom erldoc-man-index "http://www.erlang.org/doc/man_index.html" + "The URL to the man_index.html page. +Note it is advisable to customise this to a local URL for example +`file:///usr/local/19.1/lib/erlang/doc/man_index.html' to speed +up the indexing." + :type 'string + :group 'erldoc) + +(defcustom erldoc-verify-man-path nil + "If non-nil verify man path existence for `file://'." + :type 'boolean + :group 'erldoc) + +(defcustom erldoc-output-file (locate-user-emacs-file "cache/erldoc") + "File to store the parsed results." + :type 'file + :group 'erldoc) + +(defun erldoc-strip-string (s) + (let* ((re "[ \t\n\r\f\v\u00a0]+") + (from (if (string-match (concat "\\`" re) s) (match-end 0) 0)) + (to (and (string-match (concat re "\\'") s) (match-beginning 0)))) + (substring s from (and to (max to from))))) + +;; Note: don't know how to get the BASE-URL to +;; `libxml-parse-html-region' to work. +(defun erldoc-expand-url (url base-url) + (if (url-type (url-generic-parse-url url)) + url + (let* ((base (url-generic-parse-url base-url)) + (dir (directory-file-name (file-name-directory (url-filename base))))) + (setf (url-filename base) (expand-file-name url dir)) + (url-recreate-url base)))) + +(defun erldoc-parse-html (url) + (with-temp-buffer + (url-insert-file-contents url) + (libxml-parse-html-region (point-min) (point-max)))) + +(defalias 'erldoc-dom-text-node-p #'stringp) + +(defun erldoc-dom-attributes (dom) + (and (not (erldoc-dom-text-node-p dom)) (cadr dom))) + +(defun erldoc-dom-get-attribute (dom attrib-name) + (cdr (assq attrib-name (erldoc-dom-attributes dom)))) + +(defun erldoc-dom-children (dom) + (and (not (erldoc-dom-text-node-p dom)) (cddr dom))) + +(defun erldoc-dom-get-text (dom) + (let ((text (car (last (erldoc-dom-children dom))))) + (and (erldoc-dom-text-node-p text) text))) + +(defvar erldoc-dom-walk-parent nil) +(defvar erldoc-dom-walk-siblings nil) + +(defun erldoc-dom-walk (dom k) + (funcall k dom) + (let ((erldoc-dom-walk-parent dom) + (erldoc-dom-walk-siblings (unless (erldoc-dom-text-node-p dom) + (cddr dom)))) + (dolist (child erldoc-dom-walk-siblings) + (erldoc-dom-walk child k)))) + +(defun erldoc-dom-get-element (dom element-name) + (catch 'return + (erldoc-dom-walk dom (lambda (d) + (when (eq (car-safe d) element-name) + (throw 'return d)))))) + +(defun erldoc-dom-get-element-by-id (dom id) + (catch 'return + (erldoc-dom-walk dom (lambda (d) + (when (equal (erldoc-dom-get-attribute d 'id) id) + (throw 'return d)))))) + +(defun erldoc-dom-get-elements-by-id (dom id) + (let (result) + (erldoc-dom-walk dom (lambda (d) + (when (equal (erldoc-dom-get-attribute d 'id) id) + (push d result)))) + (nreverse result))) + +(defun erldoc-fix-path (url) + (if (and erldoc-verify-man-path + ;; Could only verify local files + (equal (url-type (url-generic-parse-url url)) "file")) + (let* ((obj (url-generic-parse-url url)) + (new (car (file-expand-wildcards + (replace-regexp-in-string + "-[0-9]+\\(?:[.][0-9]+\\)*" "*" + (url-filename obj)))))) + (or new (error "File %s does not exist" (url-filename obj))) + (setf (url-filename obj) new) + (url-recreate-url obj)) + url)) + +(defun erldoc-parse-man-index (url) + (let ((table (erldoc-dom-get-element (erldoc-parse-html url) 'table)) + (mans)) + (erldoc-dom-walk + table + (lambda (d) + (when (eq (car-safe d) 'a) + (let ((href (erldoc-dom-get-attribute d 'href))) + (when (and href (not (string-match-p "index\\.html\\'" href))) + (with-demoted-errors "erldoc-parse-man-index: %S" + (push (cons (erldoc-dom-get-text d) + (erldoc-fix-path (erldoc-expand-url href url))) + mans))))))) + (nreverse mans))) + +(defun erldoc-parse-man (man) + (let ((dom (erldoc-parse-html (cdr man))) + (table (make-hash-table :test #'equal))) + (erldoc-dom-walk + (erldoc-dom-get-element-by-id dom "loadscrollpos") + (lambda (d) + (let ((href (erldoc-dom-get-attribute d 'href))) + (when (and href (string-match "#" href)) + (puthash (substring href (match-end 0)) + (list (concat (car man) ":" (erldoc-strip-string + (erldoc-dom-get-text d))) + (erldoc-expand-url href (cdr man))) + table))))) + (let ((span-content + (lambda (span) + (let ((texts)) + (erldoc-dom-walk span + (lambda (d) + (and (erldoc-dom-text-node-p d) + (push (erldoc-strip-string d) texts)))) + (and texts (mapconcat 'identity (nreverse texts) " "))))) + entries) + (erldoc-dom-walk + dom + (lambda (d) + ;; Get the full function signature. + (when (and (eq (car-safe d) 'a) + (gethash (erldoc-dom-get-attribute d 'name) table)) + (push (append (gethash (erldoc-dom-get-attribute d 'name) table) + (list (funcall span-content + (or (erldoc-dom-get-element d 'span) + (cadr (memq d erldoc-dom-walk-siblings)))))) + entries)) + ;; Get data types + (when (and (eq (car-safe d) 'a) + (string-prefix-p "type-" + (or (erldoc-dom-get-attribute d 'name) ""))) + (push (list (concat (car man) ":" (funcall span-content d)) + (concat (cdr man) "#" (erldoc-dom-get-attribute d 'name)) + (funcall span-content erldoc-dom-walk-parent)) + entries)))) + entries))) + +(defun erldoc-parse-all (man-index output &optional json) + (let* ((output (expand-file-name output)) + (table (make-hash-table :size 11503 :test #'equal)) + (mans (erldoc-parse-man-index man-index)) + (progress 1) + (reporter (make-progress-reporter "Parsing Erlang/OTP documentation" + progress (length mans))) + fails all) + (dolist (man mans) + (condition-case err + (push (erldoc-parse-man man) all) + (error (push (error-message-string err) fails))) + (accept-process-output nil 0.01) + (progress-reporter-update reporter (cl-incf progress))) + (when fails + (display-warning 'erldoc-parse-all + (format "\n\n%s" (mapconcat #'identity fails "\n")) + :error)) + (progress-reporter-done reporter) + (mapc (lambda (x) (puthash (car x) (cdr x) table)) + (apply #'nconc (nreverse all))) + (with-temp-buffer + (if (not json) + (pp table (current-buffer)) + (eval-and-compile (require 'json)) + (let ((json-encoding-pretty-print t)) + (insert (json-encode table)))) + (unless (file-directory-p (file-name-directory output)) + (make-directory (file-name-directory output) t)) + (write-region nil nil output nil nil nil 'ask)))) + +(defun erldoc-otp-release () + "Get the otp release version (as string) or nil if not found." + (let ((otp (erldoc-dom-get-text + (erldoc-dom-get-element + (erldoc-parse-html + (erldoc-expand-url "index.html" erldoc-man-index)) + 'title)))) + (and (string-match "[0-9.]+\\'" otp) (match-string 0 otp)))) + +(defvar erldoc-browse-history nil) +(defvar erldoc-lookup-table nil) + +(defun erldoc-lookup-table () + (or erldoc-lookup-table + (progn + (unless (file-exists-p erldoc-output-file) + (let ((of (pcase (erldoc-otp-release) + (`nil erldoc-output-file) + (ver (concat erldoc-output-file "-" ver))))) + (unless (file-exists-p of) + (erldoc-parse-all erldoc-man-index of)) + (unless (string= erldoc-output-file of) + (make-symbolic-link of erldoc-output-file)))) + (setq erldoc-lookup-table + (with-temp-buffer + (insert-file-contents erldoc-output-file) + (read (current-buffer))))))) + +(defun erldoc-best-matches (mfa) + (pcase mfa + ((and `(,m ,f) (let a (erlang-get-function-arity))) + (let ((mfa (format "%s:%s/%s" m f a))) + (cond ((gethash mfa (erldoc-lookup-table)) (list mfa)) + (m (all-completions (concat m ":" f "/") (erldoc-lookup-table))) + (t (let* ((mod (erlang-get-module)) + (mf1 (and mod (concat mod ":" f "/"))) + (mf2 (concat "erlang:" f "/")) + (re (concat ":" (regexp-quote f) "/"))) + (or (and mf1 (all-completions mf1 (erldoc-lookup-table))) + (all-completions mf2 (erldoc-lookup-table)) + (cl-loop for k being the hash-keys of (erldoc-lookup-table) + when (string-match-p re k) + collect k))))))))) + +;;;###autoload +(defun erldoc-browse (mfa) + (interactive + (let ((default + ;; `erlang-mode-syntax-table' is lazily initialised. + (with-syntax-table (or erlang-mode-syntax-table (standard-syntax-table)) + (ignore-errors + (erldoc-best-matches + (or (erlang-get-function-under-point) + (save-excursion + (goto-char (or (cadr (syntax-ppss)) (point))) + (erlang-get-function-under-point)))))))) + (list (completing-read (format (if default "Function {%d %s} (default %s): " + "Function: ") + (length default) + (if (= (length default) 1) "guess" "guesses") + (car default)) + (erldoc-lookup-table) + nil t nil 'erldoc-browse-history default)))) + (or (stringp mfa) + (signal 'wrong-type-argument (list 'string mfa 'mfa))) + (browse-url (or (car (gethash mfa (erldoc-lookup-table))) + (user-error "No documentation for %s" mfa)))) + +;;;###autoload +(defun erldoc-apropos (pattern) + (interactive "sPattern: ") + (with-help-window (help-buffer) + (with-current-buffer standard-output + (princ (concat "Erldoc apropos pattern: " pattern "\n\n")) + (maphash (lambda (k v) + (when (string-match-p pattern k) + (insert-text-button k :type 'help-url + 'help-args (list (car v))) + (insert "\n"))) + (erldoc-lookup-table))))) + +(defun erldoc-tokenize-signature (sig) + ;; Divide SIG into (MF ARGLIST RETTYPE) + (let ((from (if (string-match "\\`.+?(" sig) + (1- (match-end 0)) + 0)) + (to (and (string-match "\\s-*->\\s-*.*?\\'" sig) (match-beginning 0)))) + (list (erldoc-strip-string (substring sig 0 from)) + (erldoc-strip-string (substring sig from (and to (max from to)))) + (and to (erldoc-strip-string (substring sig to)))))) + +(defun erldoc-format-signature (mod fn) + (when (and mod fn (or erldoc-lookup-table + (file-exists-p erldoc-output-file))) + (let ((re (concat "\\`" mod ":" fn "/\\([0-9]+\\)\\'")) + (sigs)) + (maphash (lambda (k v) + (when (string-match re k) + (push (cons (string-to-number (match-string 1 k)) + (cdr (erldoc-tokenize-signature (cadr v)))) + sigs))) + (erldoc-lookup-table)) + (when sigs + ;; Mostly single return type but there are exceptions such as + ;; `beam_lib:chunks/2,3'. + (let ((single-rettype + (cl-reduce (lambda (x1 x2) (and x1 x2 (equal x1 x2) x1)) + sigs :key #'cl-caddr)) + (sigs (sort sigs #'car-less-than-car))) + (if single-rettype + (concat mod ":" fn (mapconcat #'cadr sigs " | ") " " single-rettype) + (mapconcat (lambda (x) (concat mod ":" fn (nth 1 x) " " (nth 2 x))) + sigs "\n"))))))) + +;;;###autoload +(defun erldoc-eldoc-function () + "A function suitable for `eldoc-documentation-function'." + (save-excursion + (pcase (erlang-get-function-under-point) + (`(,_ nil) ) + (`(nil ,fn) (erldoc-format-signature "erlang" fn)) + (`(,mod ,fn) (erldoc-format-signature mod fn))))) + +(defun erldoc-parse-eeps-index () + (let* ((url "http://www.erlang.org/eeps/") + (table (catch 'return + (erldoc-dom-walk (erldoc-parse-html url) + (lambda (d) + (and (eq (car-safe d) 'table) + (equal (erldoc-dom-get-attribute d 'summary) + "Numerical Index of EEPs") + (throw 'return d)))))) + (fix-title (lambda (title) + (replace-regexp-in-string + "`` *" "" (replace-regexp-in-string " *``, *" " by " title)))) + (result)) + (erldoc-dom-walk + table (lambda (d) + (when (eq (car-safe d) 'a) + (push (cons (funcall fix-title (erldoc-dom-get-attribute d 'title)) + (erldoc-expand-url + (erldoc-dom-get-attribute d 'href) + url)) + result)))) + (nreverse result))) + +(defvar erldoc-user-guides nil) + +(defvar erldoc-missing-user-guides + '("compiler" "hipe" "kernel" "os_mon" "parsetools" "typer") + "List of standard Erlang applications with no user guides.") + +;; Search in `code:lib_dir/0' using find LIB_DIR -type f -name +;; '*_app.html'. +(defvar erldoc-app-manuals '("crypto" "diameter" "erl_docgen" + "kernel" "observer" "os_mon" + "runtime_tools" "sasl" "snmp" + "ssl" "test_server" + ("ssh" . "SSH") ("stdlib" . "STDLIB") + ("hipe" . "HiPE") ("typer" . "TypEr")) + "List of applications that come with a manual.") + +(defun erldoc-user-guide-chapters (user-guide) + (pcase-let ((`(,name . ,url) user-guide)) + (unless (member name erldoc-missing-user-guides) + (let ((chaps (erldoc-dom-get-elements-by-id + (erldoc-dom-get-element-by-id (erldoc-parse-html url) "leftnav") + "no"))) + (or chaps (warn "erldoc-user-guide-chapters no chapters found for `%s'" + (cdr user-guide))) + (mapcar (lambda (li) + (cons (concat name "#" (erldoc-dom-get-attribute li 'title)) + (erldoc-expand-url (erldoc-dom-get-attribute + (erldoc-dom-get-element li 'a) 'href) + url))) + chaps))))) + +(defun erldoc-user-guides-1 () + (let ((url (erldoc-expand-url "applications.html" erldoc-man-index)) + app-guides app-mans) + (erldoc-dom-walk + (erldoc-parse-html url) + (lambda (d) + (when (and (eq (car-safe d) 'a) + (not (string-match-p "\\`[0-9.]+\\'" (erldoc-dom-get-text d)))) + (with-demoted-errors "erldoc-user-guides-1: %S" + (let ((name (erldoc-strip-string (erldoc-dom-get-text d))) + (index-page (erldoc-fix-path (erldoc-expand-url + (erldoc-dom-get-attribute d 'href) url)))) + (push (cons name (if (member name erldoc-missing-user-guides) + index-page + (erldoc-expand-url "users_guide.html" index-page))) + app-guides) + ;; Collect application manuals. + (pcase (assoc name (mapcar (lambda (x) (if (consp x) x (cons x x))) + erldoc-app-manuals)) + (`(,_ . ,manual) + (push (cons name + (erldoc-expand-url (format "%s_app.html" manual) + index-page)) + app-mans)))))))) + (list (nreverse app-guides) + (nreverse app-mans)))) + +(defun erldoc-user-guides () + (or erldoc-user-guides + (let ((file (concat erldoc-output-file "-topics"))) + (unless (file-exists-p file) + (unless (file-directory-p (file-name-directory file)) + (make-directory (file-name-directory file) t)) + (with-temp-buffer + (pcase-let ((`(,guides ,mans) (erldoc-user-guides-1))) + (pp (append (cl-mapcan #'erldoc-user-guide-chapters + (append (mapcar + (lambda (dir) + (cons dir (erldoc-expand-url + (concat dir "/users_guide.html") + erldoc-man-index))) + '("design_principles" + "efficiency_guide" + "embedded" + "getting_started" + "installation_guide" + "oam" + "programming_examples" + "reference_manual" + "system_architecture_intro" + "system_principles" + "tutorial")) + guides)) + (mapcar (lambda (man) + (pcase-let ((`(,name . ,url) man)) + (cons (concat name " (App)") url))) + mans) + (erldoc-parse-eeps-index)) + (current-buffer))) + (write-region nil nil file nil nil nil 'ask))) + (setq erldoc-user-guides (with-temp-buffer (insert-file-contents file) + (read (current-buffer))))))) + +;;;###autoload +(defun erldoc-browse-topic (topic) + (interactive + (list (completing-read "User guide: " (erldoc-user-guides) nil t))) + (browse-url (cdr (assoc topic (erldoc-user-guides))))) + +(provide 'erldoc) +;;; erldoc.el ends here |