Every Flan form heads the code it produced in the emitted IR, the x86 listing and both disassembly buffers, and the objects are the same with or without it
This commit is contained in:
parent
bf827dc55b
commit
066fb18940
21
TODO.org
21
TODO.org
@ -1169,15 +1169,18 @@ compilation, the flagship program calls into the agent unconditionally, and
|
||||
=Reach= cannot prune a package something reachable calls into. A refusal is only
|
||||
honest when the caller has a way to not ask.
|
||||
|
||||
** TODO The IR and the disassembly are not annotated with the source
|
||||
The emitter writes =.ll= as text and every typed IR node carries a location, so an
|
||||
IR comment costs nothing and cannot break anything. The disassembly half is
|
||||
=objdump=, which the daemon already shells out to without =-S= or =-l=. Settled in
|
||||
conversation: =-O0= gets the full annotation and =-O2= gets nothing or whatever
|
||||
best-effort mapping falls out, so =--debug= forcing =-O0= is fine. Writing a
|
||||
disassembler stays off the table; richer annotation of objdump's output needs only
|
||||
what the daemon already holds, which is where SBCL's advantage actually comes
|
||||
from.
|
||||
** DONE The IR and the disassembly are not annotated with the source
|
||||
CLOSED: [2026-09-25]
|
||||
Every Flan form heads the code it produced: a comment in the =.ll= (=Emit.annot=),
|
||||
and in the x86 listing a per-function map from byte offsets to forms, written as
|
||||
comments after =.size= (=X86.srcmap=). =flan emit= and every dev build annotate;
|
||||
=--no-annotate= turns it off, and the objects are identical either way (tested).
|
||||
The daemon's =C-c C-a= places the forms from what it kept of each build: the x86
|
||||
map, or on LLVM the line table read with =objdump -l= plus the =.ll='s headings,
|
||||
which exists only under =--debug=; an =-O2= LLVM session says so in =:note=. The
|
||||
lowering buffer annotates all four sections, the two =llc= ones from a =--debug=
|
||||
copy of the IR. Rules out writing a disassembler, and reading the source off disk
|
||||
at disassembly time.
|
||||
|
||||
* Runtime
|
||||
|
||||
|
||||
14
bin/main.ml
14
bin/main.ml
@ -197,11 +197,12 @@ let x86_flag = "--x86"
|
||||
mean two different things depending on which subcommand it followed. *)
|
||||
let llvm_flag = "--llvm"
|
||||
|
||||
(* [flan emit --x86] annotates, because it exists to be read. This turns that
|
||||
off, and the only caller who wants it is the test that assembles the listing
|
||||
both ways and compares the object's sections byte for byte -- a claim that
|
||||
comments and the splitting of a [.byte] directive are invisible to the
|
||||
assembler is worth measuring rather than asserting. *)
|
||||
(* [flan emit] annotates, with --x86 or without, because it exists to be
|
||||
read: each Flan form heads the code it produced. This turns that off, and
|
||||
the callers who want it are the checks that compile the output both ways and
|
||||
compare the objects byte for byte -- a claim that comments, and the splitting
|
||||
of a [.byte] directive, are invisible to the assembler and to [llc] is worth
|
||||
measuring rather than asserting. *)
|
||||
let no_annotate_flag = "--no-annotate"
|
||||
|
||||
(* "This program carries no collector", and the way it is kept is a refusal
|
||||
@ -676,7 +677,8 @@ let () =
|
||||
on: the flag is a question asked of what was checked, never a
|
||||
parameter of what is emitted. *)
|
||||
if List.mem no_gc_flag args then Flan.Check.no_gc p;
|
||||
Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize p
|
||||
Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize
|
||||
~annotate:(not (List.mem no_annotate_flag args)) p
|
||||
|> print_string))
|
||||
files
|
||||
| _ :: "build" :: path :: rest ->
|
||||
|
||||
@ -1059,6 +1059,19 @@ you are reading.
|
||||
`C-u C-c C-l` asks for the file as well as the name, for the case where the
|
||||
function you want to read is not in the buffer you are in.
|
||||
|
||||
Every section is headed by the Flan forms it came from: each run of
|
||||
instructions has a comment above it quoting the form that produced it and where
|
||||
that form is written. The IR carries these comments itself. The two `llc`
|
||||
sections are compiled from the same IR with a line table added, which changes
|
||||
no instruction, and their line-table positions are shown as the forms. The x86
|
||||
section reads the map from byte offsets to forms that the backend writes at the
|
||||
end of each function. At `-O2` the mapping is only as good as what LLVM keeps.
|
||||
|
||||
`C-c C-a` shows source lines the same way, from what the daemon kept of each
|
||||
build. On the x86 backend, the default, every listing has them. On an LLVM
|
||||
session they need a line table, which only `flan dev --llvm --debug` has; a
|
||||
listing without one says so in its `note` line.
|
||||
|
||||
The four outputs need `llc`, `as` and `objdump` on `PATH` — the backend writes
|
||||
machine code rather than mnemonics, so its section is assembled and
|
||||
disassembled to be readable, which is also a check that the bytes are well
|
||||
|
||||
@ -177,15 +177,105 @@ reader needs."
|
||||
"The contents of FILE as a string."
|
||||
(with-temp-buffer (insert-file-contents file) (buffer-string)))
|
||||
|
||||
(defun flan-lower--ll (file flags)
|
||||
(defun flan-lower--ll (file flags &optional debug)
|
||||
"The IR for FILE under FLAGS, made once and kept.
|
||||
Every section is downstream of this one: `llc' reads it twice and the x86
|
||||
section is the same frontend asked for a different back half."
|
||||
(let ((ll (expand-file-name "out.ll" (flan-lower--scratch))))
|
||||
section is the same frontend asked for a different back half.
|
||||
|
||||
With DEBUG, the same IR with a line table in it, which is what `llc' is
|
||||
given: its output then says which source position each instruction came
|
||||
from, and `flan-lower--annotate-llc' turns those into the forms. A line
|
||||
table changes no instruction LLVM emits, so the two LLVM sections are still
|
||||
what the IR section compiles to."
|
||||
(let ((ll (expand-file-name (if debug "out.dbg.ll" "out.ll")
|
||||
(flan-lower--scratch))))
|
||||
(unless (file-exists-p ll)
|
||||
(apply #'flan-lower--run ll flan-lower-program "emit" file flags))
|
||||
(apply #'flan-lower--run ll flan-lower-program "emit" file
|
||||
(if (and debug (not (member "--debug" flags)))
|
||||
(append flags '("--debug"))
|
||||
flags)))
|
||||
ll))
|
||||
|
||||
;;; Source lines
|
||||
|
||||
;; Every listing here is headed by the Flan forms it came from. The IR carries
|
||||
;; them itself: `flan emit' writes a `; form file:line:col' comment above the
|
||||
;; first instruction of each form. The other three lose comments on the way
|
||||
;; through a compiler, so each gets them back from what survives: `llc' keeps
|
||||
;; the line table's positions as `.loc' directives, and the x86 backend ends
|
||||
;; each function with a map from byte offsets to forms, which the assembler
|
||||
;; ignores and which is read here out of the assembly it was given.
|
||||
|
||||
(defun flan-lower--headings (ir)
|
||||
"A table of position (\"file:line:col\") to form, from the comments in IR."
|
||||
(let ((tbl (make-hash-table :test #'equal)))
|
||||
(dolist (l (split-string ir "\n"))
|
||||
(when (string-match
|
||||
"\\`[ \t]*; \\(.*[^ ]\\) +\\([^ ]+:[0-9]+:[0-9]+\\)\\'" l)
|
||||
;; Both taken before `string-trim', which matches a regexp of its own.
|
||||
(let* ((where (match-string 2 l))
|
||||
(form (string-trim (match-string 1 l))))
|
||||
(unless (gethash where tbl) (puthash where form tbl)))))
|
||||
tbl))
|
||||
|
||||
(defun flan-lower--annotate-llc (text headings)
|
||||
"TEXT, `llc' output with a line table, with each position written as its form.
|
||||
A `.loc' whose position has a form in HEADINGS becomes a comment quoting it,
|
||||
unless it names the form just quoted; the directives, and the labels the line
|
||||
table alone needed, are dropped."
|
||||
(let ((last nil) (out nil))
|
||||
(dolist (l (split-string text "\n"))
|
||||
(cond
|
||||
((string-match "\\`[ \t]*\\.loc\\b\\(?:.*# \\([^ ]*:[0-9]+:[0-9]+\\)[ \t]*\\'\\)?" l)
|
||||
(let ((form (and (match-string 1 l)
|
||||
(gethash (match-string 1 l) headings))))
|
||||
(when (and form (not (equal form last)))
|
||||
(setq last form)
|
||||
(push (format "\t# %s%s%s" form
|
||||
(make-string (max 1 (- 56 (length form))) ?\s)
|
||||
(match-string 1 l))
|
||||
out))))
|
||||
((string-match-p "\\`\\(\\.Ltmp[0-9]+\\|\\.Lfunc_begin[0-9]+\\):\\'" l))
|
||||
((string-match-p "\\`[ \t]*\\.file[ \t]+[0-9]" l))
|
||||
(t (push l out))))
|
||||
(string-join (nreverse out) "\n")))
|
||||
|
||||
(defun flan-lower--srcmap (asm name)
|
||||
"The x86 backend's source map for NAME out of ASM, as (OFFSET DEPTH WHERE FORM)."
|
||||
(let ((sym (concat "flan." name)) (out nil))
|
||||
(dolist (l (split-string asm "\n"))
|
||||
(let ((f (split-string (string-trim l) "\t")))
|
||||
(when (and (>= (length f) 6)
|
||||
(equal (nth 0 f) "#@")
|
||||
(equal (nth 1 f) sym))
|
||||
(push (list (string-to-number (substring (nth 2 f) 2) 16)
|
||||
(string-to-number (nth 3 f))
|
||||
(nth 4 f)
|
||||
(string-join (nthcdr 5 f) "\t"))
|
||||
out))))
|
||||
(nreverse out)))
|
||||
|
||||
(defun flan-lower--annotate-x86 (text srcmap)
|
||||
"TEXT, objdump's listing of one function, with SRCMAP's forms placed in it.
|
||||
Each form is written above the first instruction at or past its offset from
|
||||
the function's first byte, indented by how deeply it is nested."
|
||||
(let ((base nil) (out nil) (pending srcmap))
|
||||
(dolist (l (split-string text "\n"))
|
||||
(cond
|
||||
((string-match "\\`\\([0-9a-f]+\\) <[^>]+>:" l)
|
||||
(setq base (string-to-number (match-string 1 l) 16)))
|
||||
((and base (string-match "\\`[ \t]*\\([0-9a-f]+\\):" l))
|
||||
(let ((off (- (string-to-number (match-string 1 l) 16) base)))
|
||||
(while (and pending (<= (car (car pending)) off))
|
||||
(pcase-let* ((`(,_ ,depth ,where ,form) (pop pending))
|
||||
(head (format "\t%s# %s" (make-string (* 2 depth) ?\s)
|
||||
form)))
|
||||
(push (concat head (make-string (max 1 (- 62 (length head))) ?\s)
|
||||
where)
|
||||
out))))))
|
||||
(push l out))
|
||||
(string-join (nreverse out) "\n")))
|
||||
|
||||
(defun flan-lower--fetch (section file name flags)
|
||||
"The text of SECTION for NAME in FILE, compiled with FLAGS.
|
||||
Narrowed to the one function, which is almost always what is wanted: the
|
||||
@ -198,15 +288,19 @@ prelude is emitted too, so a two-line program is ten thousand lines of IR."
|
||||
(flan-lower--narrow (flan-lower--slurp (flan-lower--ll file flags))
|
||||
(concat "^define .*@\"?" sym "\"?(") "^}"))
|
||||
((or 'O0 'O2)
|
||||
(let ((s (expand-file-name (format "out.%s.s" section) (flan-lower--scratch))))
|
||||
(let ((s (expand-file-name (format "out.%s.s" section) (flan-lower--scratch)))
|
||||
(ll (flan-lower--ll file flags t)))
|
||||
(unless (file-exists-p s)
|
||||
(flan-lower--run s flan-lower-llc
|
||||
(if (eq section 'O0) "-O0" "-O2")
|
||||
(flan-lower--ll file flags) "-o" s))
|
||||
ll "-o" s))
|
||||
;; `.size' ends the function in GAS output, and it is the last line
|
||||
;; of it rather than the first line of the next.
|
||||
(flan-lower--narrow (flan-lower--slurp s)
|
||||
(concat "^\"?" sym "\"?:") "\\.size")))
|
||||
(let ((text (flan-lower--narrow (flan-lower--slurp s)
|
||||
(concat "^\"?" sym "\"?:") "\\.size")))
|
||||
(and text
|
||||
(flan-lower--annotate-llc
|
||||
text (flan-lower--headings (flan-lower--slurp ll)))))))
|
||||
('x86
|
||||
(let ((dis (expand-file-name "out.x86.dis" (flan-lower--scratch))))
|
||||
(unless (file-exists-p dis)
|
||||
@ -224,8 +318,13 @@ prelude is emitted too, so a two-line program is ten thousand lines of IR."
|
||||
;; formed, which reading them never would be.
|
||||
(flan-lower--run o "as" "--64" "-o" o s)
|
||||
(flan-lower--run dis "objdump" "-d" "--no-show-raw-insn" o)))
|
||||
(flan-lower--narrow (flan-lower--slurp dis)
|
||||
(concat "<" sym ">:") "^$")))
|
||||
(let ((text (flan-lower--narrow (flan-lower--slurp dis)
|
||||
(concat "<" sym ">:") "^$"))
|
||||
(s (expand-file-name "out.x86.s" (flan-lower--scratch))))
|
||||
(and text
|
||||
(flan-lower--annotate-x86
|
||||
text (and (file-exists-p s)
|
||||
(flan-lower--srcmap (flan-lower--slurp s) name)))))))
|
||||
(_ (error "no such section: %s" section)))))
|
||||
|
||||
(defun flan-lower--narrow (text start end)
|
||||
@ -542,7 +641,7 @@ that is felt."
|
||||
;; their intermediates too -- they are not redrawn here, but the next `r'
|
||||
;; on one of them must not answer out of a file the old IR produced.
|
||||
(dolist (f (pcase id
|
||||
('ir '("out.ll" "out.O0.s" "out.O2.s"
|
||||
('ir '("out.ll" "out.dbg.ll" "out.O0.s" "out.O2.s"
|
||||
"out.x86.s" "out.x86.o" "out.x86.dis"))
|
||||
('O0 '("out.O0.s"))
|
||||
('O2 '("out.O2.s"))
|
||||
|
||||
@ -2926,7 +2926,16 @@ tail of exactly one packaged name, because a buffer inside a package writes
|
||||
(when (plist-get r :note)
|
||||
(flan-disassemble--header "note" (plist-get r :note)))
|
||||
(insert "\n")
|
||||
(insert (plist-get r :text))
|
||||
(let ((start (point)))
|
||||
(insert (plist-get r :text))
|
||||
;; The source lines the daemon placed in the listing, and the
|
||||
;; headings in the IR, are `;' comments; shown as comments so the
|
||||
;; code between them reads as the code.
|
||||
(save-excursion
|
||||
(goto-char start)
|
||||
(while (re-search-forward "^[ \t]*;.*$" nil t)
|
||||
(put-text-property (match-beginning 0) (match-end 0)
|
||||
'face 'font-lock-comment-face))))
|
||||
(goto-char (point-min))))
|
||||
(display-buffer flan-disassembly-buffer)))
|
||||
|
||||
|
||||
@ -1760,7 +1760,10 @@ already rely on it — so nothing here is a stand-in for the real thing."
|
||||
;; cell is still the one the process was launched with — the one case
|
||||
;; where what is installed *now* is knowable, and it says so.
|
||||
(test-flan--check "and says the program is still running the host's copy"
|
||||
(string-match-p "host executable" text))))
|
||||
(string-match-p "host executable" text))
|
||||
(test-flan--check "and the IR is headed by the forms it came from"
|
||||
(string-match-p "^ *; (set ticks (\\+ ticks 1)) +dev-repl\\.flan:[0-9]+:3$"
|
||||
text))))
|
||||
|
||||
;; Refused by name rather than shown as an empty buffer.
|
||||
(test-flan--check "a name the program does not define is refused by name"
|
||||
@ -1965,7 +1968,15 @@ already rely on it — so nothing here is a stand-in for the real thing."
|
||||
(length (alist-get 'O0 flan-lower--texts))))
|
||||
(test-flan--check "the x86 section is the backend's bytes, disassembled"
|
||||
(and (string-match-p "<flan\\.step>:" text)
|
||||
(string-match-p "push +%rbp" text))))
|
||||
(string-match-p "push +%rbp" text)))
|
||||
;; Each section headed by the source, and in the right place: the
|
||||
;; form is above the code it made, so it is not the section's last
|
||||
;; line and the IR's is followed by an instruction.
|
||||
(dolist (s '(ir O0 O2 x86))
|
||||
(test-flan--check (format "the %s section is headed by the source" s)
|
||||
(string-match-p
|
||||
"[;#] (set ticks (\\+ ticks 1)) +[^ \n]*dev-repl\\.flan:[0-9]+:3\n."
|
||||
(alist-get s flan-lower--texts)))))
|
||||
;; `r' is the other half of the caching claim: one section redrawn,
|
||||
;; and the IR every section is downstream of left where it was. The
|
||||
;; mtime is the only evidence of that, since a re-emitted out.ll would
|
||||
|
||||
@ -867,12 +867,16 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
||||
Filename.concat dir
|
||||
(Filename.basename out ^ if opts.x86 then ".s" else ".ll")
|
||||
in
|
||||
(* A dev build is annotated, because the dev daemon keeps this text beside the
|
||||
host it built and a disassembly of the host reads its source map out of
|
||||
it. A comment changes nothing an assembler or [llc] makes of the file. *)
|
||||
write ll
|
||||
(if opts.x86 then
|
||||
X86.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug p
|
||||
X86.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug
|
||||
~annotate:opts.dev p
|
||||
else
|
||||
Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug ~pnames
|
||||
~sanitize:opts.sanitize p);
|
||||
~sanitize:opts.sanitize ~annotate:opts.dev p);
|
||||
(* [flan_dev.c] is compiled into every build, not only a dev one. Nothing in
|
||||
a release build calls into it — the compiler only emits a registry lookup
|
||||
for a name the host was not built with, which cannot arise without cells —
|
||||
|
||||
173
lib/dev.ml
173
lib/dev.ml
@ -3352,10 +3352,19 @@ let rerun t =
|
||||
SBCL's presentation is worth two things here and not a third. Offsets from
|
||||
the function's own start rather than file addresses, because an address into
|
||||
a .so means nothing to a reader; and labels for branch targets inside the
|
||||
function, which is most of the difference between readable and not. The
|
||||
third is source interleaving, which SBCL can do because it has the mapping
|
||||
and this build has no line tables — so it is refused by name in the reply
|
||||
instead of being faked by printing the listing with no source in it. *)
|
||||
function, which is most of the difference between readable and not.
|
||||
|
||||
The third is source interleaving: the Flan form each run of instructions
|
||||
came from, above the run. Where the mapping comes from depends on the
|
||||
backend. The x86 backend knows the exact offset of every byte it writes, so
|
||||
an annotated build ends each function with a source map in comments, and
|
||||
the assembly the daemon keeps beside each object is where it is read from
|
||||
(see [X86.srcmap]). An LLVM build has only what LLVM keeps, which is a line
|
||||
table and only under [--debug]; the form quoted for a line is read from the
|
||||
comments in the kept [.ll] (see [Emit.annot]). An optimised LLVM build has
|
||||
no line table and its instructions are not in source order anyway, so
|
||||
there the reply says that rather than printing a listing that looks
|
||||
annotated and is not. *)
|
||||
|
||||
let objdump = try Sys.getenv "FLAN_OBJDUMP" with Not_found -> "objdump"
|
||||
|
||||
@ -3399,6 +3408,22 @@ let ir_of ~ir name =
|
||||
bytes when an instruction's encoding does not fit the column. *)
|
||||
type insn = { off : int; bytes : string; text : string }
|
||||
|
||||
(* [objdump -l] puts [path:line] (sometimes followed by a discriminator) on a
|
||||
line of its own above the first instruction of each run from that line. *)
|
||||
let line_marker l =
|
||||
let l = String.trim l in
|
||||
let l =
|
||||
match String.index_opt l ' ' with
|
||||
| Some i -> String.sub l 0 i
|
||||
| None -> l
|
||||
in
|
||||
match String.rindex_opt l ':' with
|
||||
| Some i when i > 0 && i < String.length l - 1 ->
|
||||
(match int_of_string_opt (String.sub l (i + 1) (String.length l - i - 1)) with
|
||||
| Some n when n > 0 -> Some (String.sub l 0 i, n)
|
||||
| _ -> None)
|
||||
| _ -> None
|
||||
|
||||
let parse_listing ~sym text =
|
||||
let head = "<" ^ sym ^ ">:" in
|
||||
let lines = String.split_on_char '\n' text in
|
||||
@ -3416,9 +3441,14 @@ let parse_listing ~sym text =
|
||||
let body = upto (drop lines) in
|
||||
let base = ref None in
|
||||
let out = ref [] in
|
||||
let marks = ref [] and pending = ref None in
|
||||
List.iter
|
||||
(fun l ->
|
||||
match String.split_on_char '\t' l with
|
||||
| [ one ] ->
|
||||
(match line_marker one with
|
||||
| Some m -> pending := Some m
|
||||
| None -> ())
|
||||
| addr :: bytes :: rest ->
|
||||
let a = String.trim addr in
|
||||
let a =
|
||||
@ -3431,13 +3461,18 @@ let parse_listing ~sym text =
|
||||
| Some n ->
|
||||
if !base = None then base := Some n;
|
||||
let b = match !base with Some b -> b | None -> n in
|
||||
(match !pending with
|
||||
| Some (file, line) ->
|
||||
marks := (n - b, file, line) :: !marks;
|
||||
pending := None
|
||||
| None -> ());
|
||||
out :=
|
||||
{ off = n - b; bytes = String.trim bytes;
|
||||
text = String.trim (String.concat "\t" rest) }
|
||||
:: !out)
|
||||
| _ -> ())
|
||||
body;
|
||||
(List.rev !out, !base <> None)
|
||||
(List.rev !out, List.rev !marks, !base <> None)
|
||||
|
||||
(* A branch inside the function shows as [<flan.step+0x79>] or, for the entry,
|
||||
[<flan.step>]. Those become [L0]..[Ln] in address order, as SBCL labels
|
||||
@ -3461,7 +3496,58 @@ let target_of ~sym text =
|
||||
int_of_string_opt (String.sub inner (p + 1) (String.length inner - p - 1))
|
||||
else None
|
||||
|
||||
let render_listing ~sym insns =
|
||||
(* One source line of a listing, in the listing's comment syntax. *)
|
||||
let src_line ~depth ~where form =
|
||||
let head = Printf.sprintf "; %s%s" (String.make (2 * depth) ' ') form in
|
||||
head ^ String.make (max 1 (60 - String.length head)) ' ' ^ where
|
||||
|
||||
(* The x86 backend's source map for [name], out of the assembly the module was
|
||||
built from. *)
|
||||
let x86_source ~asm name =
|
||||
List.map
|
||||
(fun (off, depth, where, form) -> (off, src_line ~depth ~where form))
|
||||
(X86.read_srcmap ~asm (Mangle.sym name))
|
||||
|
||||
(* The headings [Emit] wrote into a [.ll], by file and line: for each line the
|
||||
outermost form written on it, which is the one with the smallest column. *)
|
||||
let ll_headings ir =
|
||||
let tbl = Hashtbl.create 64 in
|
||||
List.iter
|
||||
(fun l ->
|
||||
let l = String.trim l in
|
||||
if String.length l > 2 && l.[0] = ';' then
|
||||
match String.rindex_opt l ' ' with
|
||||
| None -> ()
|
||||
| Some i ->
|
||||
let where = String.sub l (i + 1) (String.length l - i - 1) in
|
||||
let form = String.trim (String.sub l 1 i) in
|
||||
(match String.split_on_char ':' where with
|
||||
| [ file; line; col ] ->
|
||||
(match int_of_string_opt line, int_of_string_opt col with
|
||||
| Some line, Some col ->
|
||||
let key = (file, line) in
|
||||
(match Hashtbl.find_opt tbl key with
|
||||
| Some (c, _) when c <= col -> ()
|
||||
| _ -> Hashtbl.replace tbl key (col, form))
|
||||
| _ -> ())
|
||||
| _ -> ()))
|
||||
(String.split_on_char '\n' ir);
|
||||
tbl
|
||||
|
||||
(* A line table's rows as source lines: the form is looked up in the [.ll]'s
|
||||
headings, and a line with no heading is named by its position alone. *)
|
||||
let llvm_source ~ir marks =
|
||||
let tbl = ll_headings ir in
|
||||
List.map
|
||||
(fun (off, file, line) ->
|
||||
let base = Filename.basename file in
|
||||
let where = Printf.sprintf "%s:%d" base line in
|
||||
match Hashtbl.find_opt tbl (base, line) with
|
||||
| Some (_, form) -> (off, src_line ~depth:0 ~where form)
|
||||
| None -> (off, "; " ^ where))
|
||||
marks
|
||||
|
||||
let render_listing ?(src = []) ~sym insns =
|
||||
let targets =
|
||||
List.sort_uniq compare
|
||||
(List.filter_map (fun i -> target_of ~sym i.text) insns)
|
||||
@ -3474,11 +3560,24 @@ let render_listing ~sym insns =
|
||||
idx 0 targets
|
||||
in
|
||||
let b = Buffer.create 4096 in
|
||||
(* A source line is written above the first instruction at or past its
|
||||
offset, after the instruction's label: the label names the address, and
|
||||
the source says what the code from there on is. *)
|
||||
let src = ref (List.stable_sort (fun (a, _) (b, _) -> compare a b) src) in
|
||||
List.iter
|
||||
(fun i ->
|
||||
(match label i.off with
|
||||
| Some lb -> Buffer.add_string b (lb ^ ":\n")
|
||||
| None -> ());
|
||||
let rec due () =
|
||||
match !src with
|
||||
| (o, l) :: rest when o <= i.off ->
|
||||
Buffer.add_string b (l ^ "\n");
|
||||
src := rest;
|
||||
due ()
|
||||
| _ -> ()
|
||||
in
|
||||
due ();
|
||||
let text =
|
||||
match target_of ~sym i.text with
|
||||
| Some n ->
|
||||
@ -3510,13 +3609,17 @@ let render_listing ~sym insns =
|
||||
insns;
|
||||
Buffer.contents b
|
||||
|
||||
let asm_of ~obj name =
|
||||
(* The listing, and the source lines placed in it: from [source] when it has
|
||||
any, otherwise from the object's own line table, which only an LLVM
|
||||
[--debug] build has. Answers the text and whether any source was placed. *)
|
||||
let asm_of ?(source = []) ?ir ~obj name =
|
||||
let sym = Mangle.sym name in
|
||||
let code, text =
|
||||
run_capture
|
||||
(String.concat " "
|
||||
[ Filename.quote objdump; "-d";
|
||||
"--disassemble=" ^ Filename.quote sym; Filename.quote obj ])
|
||||
([ Filename.quote objdump; "-d" ]
|
||||
@ (if ir <> None then [ "-l" ] else [])
|
||||
@ [ "--disassemble=" ^ Filename.quote sym; Filename.quote obj ]))
|
||||
in
|
||||
if code <> 0 then
|
||||
Error
|
||||
@ -3524,8 +3627,14 @@ let asm_of ~obj name =
|
||||
(String.trim text))
|
||||
else
|
||||
match parse_listing ~sym text with
|
||||
| _, false -> Error (Printf.sprintf "%s found no symbol %s in %s" objdump sym obj)
|
||||
| insns, true -> Ok (render_listing ~sym insns)
|
||||
| _, _, false -> Error (Printf.sprintf "%s found no symbol %s in %s" objdump sym obj)
|
||||
| insns, marks, true ->
|
||||
let src =
|
||||
match source, ir with
|
||||
| [], Some ir -> llvm_source ~ir marks
|
||||
| s, _ -> s
|
||||
in
|
||||
Ok (render_listing ~src ~sym insns, src <> [])
|
||||
|
||||
(* Where a name's body was last built, and how much of that is a claim about
|
||||
the running process rather than about this daemon's disk. *)
|
||||
@ -3643,16 +3752,37 @@ let disassemble t ~name ~form =
|
||||
else if not (Sys.file_exists o.oso) then
|
||||
error ("the object this body was linked into is gone: " ^ o.oso)
|
||||
else
|
||||
match asm_of ~obj:o.oso name with
|
||||
| Ok text ->
|
||||
(* The kept source is what the source lines come from: the assembly's
|
||||
map on x86, the [.ll]'s headings for an LLVM line table. Missing is
|
||||
not an error — the listing is still the listing. *)
|
||||
let kept = try Some (read_file o.oll) with Sys_error _ -> None in
|
||||
let x86 = t.session.Session.x86 in
|
||||
let source =
|
||||
match kept with
|
||||
| Some asm when x86 -> x86_source ~asm name
|
||||
| _ -> []
|
||||
in
|
||||
let ir = if x86 then None else Some (Option.value ~default:"" kept) in
|
||||
match asm_of ~source ?ir ~obj:o.oso name with
|
||||
| Ok (text, placed) ->
|
||||
let note =
|
||||
if placed then []
|
||||
else if x86 then
|
||||
[ ":note "
|
||||
^ Wire.quote
|
||||
("no source map for this function in " ^ o.oll
|
||||
^ ", so the listing has no source lines") ]
|
||||
else
|
||||
[ ":note "
|
||||
^ Wire.quote
|
||||
"source interleaving needs line tables, which an LLVM \
|
||||
session has only when started with flan dev --llvm --debug" ]
|
||||
in
|
||||
ok
|
||||
(common
|
||||
@ [ ":object " ^ Wire.quote o.oso;
|
||||
":note "
|
||||
^ Wire.quote
|
||||
"source interleaving needs line tables this build does not \
|
||||
emit";
|
||||
":text " ^ Wire.quote text ])
|
||||
@ [ ":object " ^ Wire.quote o.oso ]
|
||||
@ note
|
||||
@ [ ":text " ^ Wire.quote text ])
|
||||
| Error m -> error m
|
||||
|
||||
(* ── The watch table ───────────────────────────────────────────────── *)
|
||||
@ -5137,11 +5267,12 @@ let merged_executable ~opts ~csrcs ~lflags ~pnames (p : Tast.program) ~out ~ll =
|
||||
write ll
|
||||
(if opts.x86 then
|
||||
rename_program_main_asm
|
||||
(X86.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug p)
|
||||
(X86.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug
|
||||
~annotate:opts.dev p)
|
||||
else
|
||||
rename_program_main
|
||||
(Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug
|
||||
~pnames ~sanitize:opts.sanitize p));
|
||||
~pnames ~sanitize:opts.sanitize ~annotate:opts.dev p));
|
||||
let cc src name = compile_c ~opts ~tflags ~src ~name () in
|
||||
let objs =
|
||||
(cc Runtime_src.source "flan_rt.c"
|
||||
|
||||
141
lib/emit.ml
141
lib/emit.ml
@ -430,6 +430,11 @@ type m = {
|
||||
attribute asks a pass to produce them. UBSan over this .ll covers the C
|
||||
and nothing else. *)
|
||||
sanitize : bool;
|
||||
(* True when the module is written to be read: every Flan form that emits an
|
||||
instruction is preceded by a comment quoting it and naming its position.
|
||||
A comment is nothing to [llc], so the object is the same either way; see
|
||||
[annot]. *)
|
||||
ann : bool;
|
||||
mutable nstr : int;
|
||||
(* The frame descriptors a dev build's shadow stack points at, counted apart
|
||||
from [nstr] deliberately. [nstr] is the test [redefinition] uses to decide
|
||||
@ -975,6 +980,13 @@ type f = {
|
||||
failure, the handler push and pop, the transfer guards -- none of which
|
||||
would remember to ask. *)
|
||||
mutable dloc : string;
|
||||
(* The source headings waiting for the instruction they are about, oldest
|
||||
first, each with the serial [annot] handed out for it; and the last one
|
||||
written, so that a macro's forty forms at one call site are headed once.
|
||||
See [annot]. *)
|
||||
mutable aq : (int * string) list;
|
||||
mutable alast : string;
|
||||
mutable adepth : int;
|
||||
}
|
||||
|
||||
let fresh f = f.n <- f.n + 1; Printf.sprintf "%%t%d" f.n
|
||||
@ -982,9 +994,21 @@ let fresh_label f name = f.n <- f.n + 1; Printf.sprintf "%s%d" name f.n
|
||||
|
||||
(* Nothing may follow a terminator, so emission after one is dropped: the code
|
||||
is unreachable and LLVM would reject it. *)
|
||||
(* An instruction is about to be written, so every heading queued is about it. *)
|
||||
let ann_due f =
|
||||
if f.aq <> [] then begin
|
||||
List.iter (fun (_, l) -> Buffer.add_string f.b l) f.aq;
|
||||
f.aq <- []
|
||||
end
|
||||
|
||||
let ins f fmt =
|
||||
Printf.ksprintf
|
||||
(fun s -> if f.live then Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n")) fmt
|
||||
(fun s ->
|
||||
if f.live then begin
|
||||
ann_due f;
|
||||
Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n")
|
||||
end)
|
||||
fmt
|
||||
|
||||
(* A fixed array has no padding between elements, so its zero value is exactly
|
||||
a run of zero bytes. Naming that operation lets LLVM choose its bulk-clear
|
||||
@ -1049,7 +1073,10 @@ let word_of_pattern (v : int32) =
|
||||
let term f fmt =
|
||||
Printf.ksprintf
|
||||
(fun s ->
|
||||
if f.live then Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n");
|
||||
if f.live then begin
|
||||
ann_due f;
|
||||
Buffer.add_string f.b (" " ^ s ^ f.dloc ^ "\n")
|
||||
end;
|
||||
f.live <- false)
|
||||
fmt
|
||||
|
||||
@ -1955,6 +1982,65 @@ let fcmp_op = function
|
||||
| Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge"
|
||||
| _ -> assert false
|
||||
|
||||
(* ── Source headings ──
|
||||
|
||||
In an annotated module every Flan form is headed by a comment quoting it and
|
||||
naming where it was written, placed above the first instruction it emits, so
|
||||
the IR reads as the source it came from. Queued rather than written, because
|
||||
a form that emits nothing must not leave its heading on the next form's
|
||||
code; the first instruction written flushes the queue, and a form that
|
||||
finished without one withdraws what it queued.
|
||||
|
||||
An atom — a literal, a local, a global — is not headed: it would steal the
|
||||
heading of the form that uses it. A form the checker invented has line 0 and
|
||||
no text to quote, and inherits its parent's heading. A form at the position
|
||||
the last heading named is skipped, which keeps a macro's expansion from
|
||||
repeating its call site once per form. [X86] heads its listing by the same
|
||||
rules. *)
|
||||
let atomic (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
| Tast.Int _ | Tast.Bool _ | Tast.Float _ | Tast.Str _ | Tast.Unit
|
||||
| Tast.Zero _ | Tast.None_ | Tast.Uninit _ | Tast.Local _ | Tast.Global _
|
||||
| Tast.FnAddr _ -> true
|
||||
| _ -> false
|
||||
|
||||
(* The heading's two halves: the form's text, with the macro it came out of if
|
||||
it did, and its position. *)
|
||||
let heading (loc : Loc.t) =
|
||||
match Loc.snippet loc with
|
||||
| None -> None
|
||||
| Some src ->
|
||||
let where = Printf.sprintf "%s:%d:%d" (Filename.basename loc.Loc.file)
|
||||
loc.Loc.line loc.Loc.col in
|
||||
let src =
|
||||
match loc.Loc.macro with
|
||||
| Some m -> Printf.sprintf "%s [from the macro %s]" src m
|
||||
| None -> src
|
||||
in
|
||||
Some (where, src)
|
||||
|
||||
let aserial = ref 0
|
||||
|
||||
let annot f (e : Tast.expr) =
|
||||
if atomic e || e.Tast.loc.Loc.line = 0 then None
|
||||
else
|
||||
match heading e.Tast.loc with
|
||||
| None -> None
|
||||
| Some (where, src) ->
|
||||
let key = where ^ " " ^ src in
|
||||
if key = f.alast then None
|
||||
else begin
|
||||
let prev = f.alast in
|
||||
f.alast <- key;
|
||||
incr aserial;
|
||||
let head =
|
||||
Printf.sprintf " ; %s%s" (String.make (2 * min 12 f.adepth) ' ') src
|
||||
in
|
||||
let pad = max 1 (64 - String.length head) in
|
||||
f.aq <- f.aq @ [ (!aserial, head ^ String.make pad ' ' ^ where ^ "\n") ];
|
||||
Some (!aserial, prev)
|
||||
end
|
||||
|
||||
(* Every [Tast] node already carries the position it was read from, and until
|
||||
now nothing wrote them out. The location is set for the duration of a node's
|
||||
own emission and restored afterwards, so instructions a parent emits *after*
|
||||
@ -1962,14 +2048,25 @@ let fcmp_op = function
|
||||
attributed to the parent and not to whatever ran last inside it. *)
|
||||
let rec value f (e : Tast.expr) : string =
|
||||
let v =
|
||||
match f.dsub with
|
||||
| None -> value_at f e
|
||||
| Some _ ->
|
||||
let saved = f.dloc in
|
||||
at_loc f e.Tast.loc;
|
||||
let v = value_at f e in
|
||||
f.dloc <- saved;
|
||||
if not f.md.ann then value_located f e
|
||||
else begin
|
||||
let d = f.adepth in
|
||||
let h = annot f e in
|
||||
f.adepth <- d + 1;
|
||||
let v = value_located f e in
|
||||
f.adepth <- d;
|
||||
(match h with
|
||||
| Some (s, prev) ->
|
||||
(* Withdrawn if the form wrote no instruction, a [Unit] in statement
|
||||
position or a local read: a heading left standing would be read as
|
||||
belonging to whatever came next. *)
|
||||
if List.exists (fun (k, _) -> k = s) f.aq then begin
|
||||
f.aq <- List.filter (fun (k, _) -> k < s) f.aq;
|
||||
f.alast <- prev
|
||||
end
|
||||
| None -> ());
|
||||
v
|
||||
end
|
||||
in
|
||||
(* An operand held while a sibling may allocate: spilled into a root slot the
|
||||
instant it exists, the same move a call's result gets in [call_through].
|
||||
@ -1981,6 +2078,16 @@ let rec value f (e : Tast.expr) : string =
|
||||
end;
|
||||
v
|
||||
|
||||
and value_located f (e : Tast.expr) : string =
|
||||
match f.dsub with
|
||||
| None -> value_at f e
|
||||
| Some _ ->
|
||||
let saved = f.dloc in
|
||||
at_loc f e.Tast.loc;
|
||||
let v = value_at f e in
|
||||
f.dloc <- saved;
|
||||
v
|
||||
|
||||
(* The [!DILocation] for a position, memoised: a loop body emits the same few
|
||||
lines over and over and each would otherwise make its own node. *)
|
||||
and at_loc f (loc : Loc.t) =
|
||||
@ -3547,6 +3654,7 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
||||
dsub;
|
||||
dline = (if fn.Tast.floc.Loc.line = 0 then 1 else fn.Tast.floc.Loc.line);
|
||||
dloc = "";
|
||||
aq = []; alast = ""; adepth = 0;
|
||||
} in
|
||||
(* Every slot is an alloca in the entry block, because [addr] may take the
|
||||
address of any of them and mem2reg only promotes entry-block allocas. *)
|
||||
@ -4525,14 +4633,14 @@ let new_dbg (p : Tast.program) =
|
||||
d
|
||||
|
||||
let new_module ~checks ~dev ~known ?(debug = false) ?(sanitize = false)
|
||||
(p : Tast.program) =
|
||||
?(annotate = false) (p : Tast.program) =
|
||||
let m = {
|
||||
out = Buffer.create 8192; strs = Buffer.create 512;
|
||||
structs = Hashtbl.create 16; datas = Hashtbl.create 16;
|
||||
unions = Hashtbl.create 16;
|
||||
globals = Hashtbl.create 16;
|
||||
externs = Hashtbl.create 32;
|
||||
checks; dev; known; nstr = 0; nfi = 0; sanitize;
|
||||
checks; dev; known; nstr = 0; nfi = 0; sanitize; ann = annotate;
|
||||
descs = Hashtbl.create 8;
|
||||
dbg = (if debug then Some (new_dbg p) else None);
|
||||
} in
|
||||
@ -4768,7 +4876,7 @@ let macro_thunk m (fn : Tast.fn) =
|
||||
copy where there is one, which is what keeps [flan_exit_hook] the merged
|
||||
build installed in reach of a trap raised inside an expansion. *)
|
||||
let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
||||
?(sanitize = false) ?(macros = []) ?(hidden = false)
|
||||
?(sanitize = false) ?(macros = []) ?(hidden = false) ?(annotate = false)
|
||||
(p : Tast.program) : string =
|
||||
(* [hidden] and [dev] are opposites and the refusal is here so that they
|
||||
cannot be written together by accident. A dev build's whole point is that
|
||||
@ -4780,7 +4888,9 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
||||
internal
|
||||
"Emit.program was given ~hidden and ~dev together, and a dev build has \
|
||||
to export its cells";
|
||||
let m = new_module ~checks ~dev ~known:(fun _ -> true) ~debug ~sanitize p in
|
||||
let m =
|
||||
new_module ~checks ~dev ~known:(fun _ -> true) ~debug ~sanitize ~annotate p
|
||||
in
|
||||
(* One cell per function, initialised to the function this build compiled.
|
||||
Nothing has been redefined yet, so a dev build starts out behaving exactly
|
||||
like a release one — the indirection is the only difference. *)
|
||||
@ -4914,7 +5024,8 @@ let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
||||
constants, and omitting them is an undefined [@.str.N] at link time. *)
|
||||
let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
|
||||
?(known = fun _ -> true) ?(retains = true)
|
||||
?call ?(consts = []) (p : Tast.program) ~fns : string =
|
||||
?call ?(consts = []) ?(annotate = false) (p : Tast.program) ~fns
|
||||
: string =
|
||||
let target name =
|
||||
match List.find_opt (fun (f : Tast.fn) -> f.Tast.name = name) p.Tast.fns with
|
||||
| Some f -> f
|
||||
@ -4945,7 +5056,7 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
|
||||
let siblings =
|
||||
List.filter (fun (f : Tast.fn) -> f.Tast.fparent = None) p.Tast.fns
|
||||
in
|
||||
let m = new_module ~checks ~dev ~known ~debug p in
|
||||
let m = new_module ~checks ~dev ~known ~debug ~annotate p in
|
||||
(* A thunk the module runs itself is excluded from all of this: it is called
|
||||
directly by [flan_reload_call], so it needs no cell, must not be published
|
||||
into one, and must not take a registry slot — there are 4096 of those and
|
||||
|
||||
29
lib/loc.ml
29
lib/loc.ml
@ -257,6 +257,35 @@ let lines_of file =
|
||||
|
||||
let forget_sources () = Hashtbl.reset source_cache
|
||||
|
||||
(** Take [text] as the current contents of [file] from its first non-blank line
|
||||
on. An editor sends a form padded with newlines so that its positions are
|
||||
the buffer's own; the padding says nothing about those lines, so what was
|
||||
known of them is kept, and every line from the form on is the text that was
|
||||
sent. That is what a snippet quoted while that form is compiled has to
|
||||
show: the buffer may be unsaved, and the file may not exist at all. *)
|
||||
let remember ~file text =
|
||||
let sent = Array.of_list (String.split_on_char '\n' text) in
|
||||
let first =
|
||||
let rec go i =
|
||||
if i >= Array.length sent then None
|
||||
else if String.trim sent.(i) <> "" then Some i
|
||||
else go (i + 1)
|
||||
in
|
||||
go 0
|
||||
in
|
||||
match first with
|
||||
| None -> ()
|
||||
| Some first ->
|
||||
let old = match lines_of file with Some ls -> ls | None -> [||] in
|
||||
let n = max (Array.length old) (Array.length sent) in
|
||||
let merged =
|
||||
Array.init n (fun i ->
|
||||
if i >= first && i < Array.length sent then sent.(i)
|
||||
else if i < Array.length old then old.(i)
|
||||
else "")
|
||||
in
|
||||
Hashtbl.replace source_cache file (Some merged)
|
||||
|
||||
let source_line (t : t) =
|
||||
if t.line <= 0 then None
|
||||
else
|
||||
|
||||
@ -484,11 +484,11 @@ type change = {
|
||||
let redefinition (t : t) ?retains ?call ?(consts = []) program ~fns =
|
||||
if not t.x86 then
|
||||
Emit.redefinition ~dev:true ~debug:t.debug ~known:(known t) ?retains
|
||||
~consts ?call program ~fns
|
||||
~consts ?call ~annotate:true program ~fns
|
||||
else
|
||||
match
|
||||
X86.redefinition ~checks:true ~dev:true ~known:(known t) ?retains ~consts
|
||||
?call program ~fns
|
||||
?call ~annotate:true program ~fns
|
||||
with
|
||||
| asm -> asm
|
||||
(* The dev backend covers a subset of the IR and refuses the rest by name,
|
||||
@ -583,6 +583,9 @@ let restore t h =
|
||||
|
||||
let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
let forms = Reader.read_all ~file:origin src in
|
||||
(* What an annotated listing quotes for this form is what was sent, not what
|
||||
the file on disk said when it was last read. *)
|
||||
Loc.remember ~file:origin src;
|
||||
Parse.with_imported ~decls:(package_decls t) t.macros @@ fun () ->
|
||||
(* Through [Load] like any other source, so an evaluated (import ...) means
|
||||
what it means in a file. Its expansion is what gets spliced, which is also
|
||||
@ -2115,6 +2118,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
||||
| [] -> fail Loc.unknown "nothing to evaluate"
|
||||
| _ :: f :: _ -> fail f.Form.loc "one expression at a time"
|
||||
in
|
||||
Loc.remember ~file:origin src;
|
||||
(* [Parse.expr] expands, so the imported set has to be in front of it here
|
||||
exactly as [eval] puts it in front of a declaration: C-x C-e sends one
|
||||
expression with no import in sight, and the session is the only thing
|
||||
|
||||
95
lib/x86.ml
95
lib/x86.ml
@ -109,7 +109,14 @@ type buf = {
|
||||
next form's. So [annote] queues, the first byte written afterwards flushes
|
||||
the queue, and [unannote] withdraws whatever its own form queued and never
|
||||
spent. *)
|
||||
mutable ann : (int * string) list;
|
||||
mutable ann : (int * string * string option) list;
|
||||
(* The source headings already written, newest first, each with the offset
|
||||
of the byte it stands above: [(n, entry)], where [entry] is the
|
||||
tab-separated [depth where form] that [srcmap] writes out. This is the map from machine
|
||||
code back to source that a disassembly of the object needs and that
|
||||
nothing in the object carries outside a [--debug] build. [n] is exact for
|
||||
the same reason every offset here is. *)
|
||||
mutable spent : (int * string) list;
|
||||
(* How far to indent a run of bytes, which is the nesting depth of the form
|
||||
that is emitting it. An argument's bytes step in and the call's step back
|
||||
out, so the shape of the expression is visible in the left margin without
|
||||
@ -118,7 +125,7 @@ type buf = {
|
||||
}
|
||||
|
||||
let create () =
|
||||
{ out = Buffer.create 4096; pend = []; n = 0; ann = []; ind = "" }
|
||||
{ out = Buffer.create 4096; pend = []; n = 0; ann = []; spent = []; ind = "" }
|
||||
|
||||
let flush b =
|
||||
if b.pend <> [] then begin
|
||||
@ -142,9 +149,12 @@ let ann_due b =
|
||||
if b.ann <> [] then begin
|
||||
flush b;
|
||||
List.iter
|
||||
(fun (s, line) ->
|
||||
(fun (s, line, src) ->
|
||||
Buffer.add_string b.out line;
|
||||
Buffer.add_char b.out '\n';
|
||||
(match src with
|
||||
| Some e -> b.spent <- (b.n, e) :: b.spent
|
||||
| None -> ());
|
||||
if s > !annmax then annmax := s)
|
||||
b.ann;
|
||||
b.ann <- []
|
||||
@ -189,15 +199,15 @@ let set_ind b s = if b.ind <> s then begin flush b; b.ind <- s end
|
||||
(* Queue one comment line. [pre] is written as given — the callers below spell
|
||||
their own leading tab and hash — and the serial comes back so that the form
|
||||
that queued it can withdraw it if it turns out to have emitted nothing. *)
|
||||
let annote b line =
|
||||
let annote ?src b line =
|
||||
incr annser;
|
||||
b.ann <- b.ann @ [ (!annser, line) ];
|
||||
b.ann <- b.ann @ [ (!annser, line, src) ];
|
||||
!annser
|
||||
|
||||
(* Drop every heading queued at or after [s] and still unspent. Answers whether
|
||||
[s] itself was spent, which is the only thing a caller wants to know. *)
|
||||
let unannote b s =
|
||||
if b.ann <> [] then b.ann <- List.filter (fun (k, _) -> k < s) b.ann;
|
||||
if b.ann <> [] then b.ann <- List.filter (fun (k, _, _) -> k < s) b.ann;
|
||||
!annmax >= s
|
||||
|
||||
(* ── Registers ───────────────────────────────────────────────────────── *)
|
||||
@ -480,7 +490,7 @@ let layout_ctx ~checks ~dev (p : Tast.program) : Emit.m =
|
||||
p.Tast.globals;
|
||||
{ Emit.out = Buffer.create 1; strs = Buffer.create 1; structs; datas; unions;
|
||||
globals; externs = Hashtbl.create 1; checks;
|
||||
dev; known = (fun _ -> true); dbg = None; sanitize = false;
|
||||
dev; known = (fun _ -> true); dbg = None; sanitize = false; ann = false;
|
||||
nstr = 0; nfi = 0; descs = Hashtbl.create 8 }
|
||||
|
||||
let sizeof md t = fst (Emit.lay md t)
|
||||
@ -889,27 +899,14 @@ let bnote ann b s =
|
||||
And a form at the same source position as the last heading written is
|
||||
skipped, which is what keeps a macro from printing its call site once per
|
||||
form of its expansion. *)
|
||||
let atomic (e : Tast.expr) =
|
||||
match e.Tast.e with
|
||||
| Tast.Int _ | Tast.Bool _ | Tast.Float _ | Tast.Str _ | Tast.Unit
|
||||
| Tast.Zero _ | Tast.None_ | Tast.Uninit _ | Tast.Local _ | Tast.Global _
|
||||
| Tast.FnAddr _ -> true
|
||||
| _ -> false
|
||||
let atomic = Emit.atomic
|
||||
|
||||
let annot f (e : Tast.expr) =
|
||||
if (not f.ann) || atomic e || e.Tast.loc.Loc.line = 0 then None
|
||||
else
|
||||
let loc = e.Tast.loc in
|
||||
let where = Printf.sprintf "%s:%d:%d" (Filename.basename loc.Loc.file)
|
||||
loc.Loc.line loc.Loc.col in
|
||||
match Loc.snippet loc with
|
||||
match Emit.heading e.Tast.loc with
|
||||
| None -> None
|
||||
| Some src ->
|
||||
let src =
|
||||
match loc.Loc.macro with
|
||||
| Some m -> Printf.sprintf "%s [from the macro %s]" src m
|
||||
| None -> src
|
||||
in
|
||||
| Some (where, src) ->
|
||||
let key = where ^ " " ^ src in
|
||||
if key = f.alast then None
|
||||
else begin
|
||||
@ -917,7 +914,8 @@ let annot f (e : Tast.expr) =
|
||||
set_ind f.b (String.make (2 * min 12 f.adepth) ' ');
|
||||
let head = Printf.sprintf "\t%s# %s" f.b.ind src in
|
||||
let pad = max 1 (62 - String.length head) in
|
||||
Some (annote f.b (head ^ String.make pad ' ' ^ where))
|
||||
let src = Printf.sprintf "%d\t%s\t%s" (min 12 f.adepth) where src in
|
||||
Some (annote ~src f.b (head ^ String.make pad ' ' ^ where))
|
||||
end
|
||||
|
||||
(* Bump-allocate a frame temporary and answer its rbp-relative offset. The
|
||||
@ -3687,6 +3685,44 @@ let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
|
||||
Buffer.add_string b (bar ^ "\n");
|
||||
Buffer.contents b
|
||||
|
||||
(* The source map of one function, written after it as comments: one line per
|
||||
source heading, giving the offset from the function's first byte of the
|
||||
instruction the heading stands above. The assembler discards all of it, so
|
||||
the object is the same; what reads it is the dev daemon, which keeps each
|
||||
module's assembly beside its object and has no line table to ask instead,
|
||||
and the lowering buffer in the editor. [base] is the prologue's length,
|
||||
since the body's offsets were counted from the end of it. *)
|
||||
let srcmap_mark = "#@"
|
||||
|
||||
let srcmap out ~sym ~base spent =
|
||||
if spent <> [] then begin
|
||||
Buffer.add_string out
|
||||
(Printf.sprintf
|
||||
"\t# The source map of %s: offset from its first byte, nesting depth, \
|
||||
position, form.\n" sym);
|
||||
List.iter
|
||||
(fun (n, e) ->
|
||||
Buffer.add_string out
|
||||
(Printf.sprintf "\t%s\t%s\t0x%x\t%s\n" srcmap_mark sym (base + n) e))
|
||||
(List.rev spent)
|
||||
end
|
||||
|
||||
(* The map [srcmap] wrote for [sym], read back out of an assembly file:
|
||||
[(offset, depth, position, form)] in the order the headings were written,
|
||||
which is address order. Empty when the file was not annotated. *)
|
||||
let read_srcmap ~asm sym =
|
||||
List.filter_map
|
||||
(fun l ->
|
||||
match String.split_on_char '\t' (String.trim l) with
|
||||
| m :: s :: off :: depth :: where :: rest
|
||||
when m = srcmap_mark && s = sym ->
|
||||
(match int_of_string_opt off, int_of_string_opt depth with
|
||||
| Some off, Some depth ->
|
||||
Some (off, depth, where, String.concat "\t" rest)
|
||||
| _ -> None)
|
||||
| _ -> None)
|
||||
(String.split_on_char '\n' asm)
|
||||
|
||||
let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
?(slot = fun _ -> None) ?(hidden = false) ?(ann = false) ?dw (fn : Tast.fn)
|
||||
: string * string =
|
||||
@ -4218,7 +4254,9 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
| None -> ());
|
||||
(match dw with Some d -> d.dcur <- None | None -> ());
|
||||
if cfi then Buffer.add_string out "\t.cfi_endproc\n";
|
||||
Buffer.add_string out (Printf.sprintf "\t.size\t%s, . - %s\n\n" sym sym);
|
||||
Buffer.add_string out (Printf.sprintf "\t.size\t%s, . - %s\n" sym sym);
|
||||
if ann then srcmap out ~sym:(Mangle.sym fn.Tast.name) ~base:pb.n f.b.spent;
|
||||
Buffer.add_char out '\n';
|
||||
Buffer.contents out, Buffer.contents f.rodata
|
||||
|
||||
(* Two functions, because the two halves run at different times and have to.
|
||||
@ -5002,7 +5040,8 @@ let program ~checks ?(dev = false) ?(debug = false) ?(annotate = false)
|
||||
class-registration thunk a redefined [defclass] carries go through it, and
|
||||
[flan dev] takes this backend unasked. *)
|
||||
let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
?(retains = true) ?(consts = []) ?call (p : Tast.program) ~fns : string =
|
||||
?(retains = true) ?(consts = []) ?call ?(annotate = false)
|
||||
(p : Tast.program) ~fns : string =
|
||||
if not dev then
|
||||
unsupported
|
||||
"x86 redefinition without cells: there is nothing to publish a body \
|
||||
@ -5096,7 +5135,9 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
\t.text\n\n";
|
||||
List.iter
|
||||
(fun (f : Tast.fn) ->
|
||||
let t, r = emit_fn md ~externs ~fns:fnstbl ~ext ~slot ~hidden:true f in
|
||||
let t, r =
|
||||
emit_fn md ~externs ~fns:fnstbl ~ext ~slot ~hidden:true ~ann:annotate f
|
||||
in
|
||||
Buffer.add_string text t;
|
||||
Buffer.add_string rodata r)
|
||||
(lifted @ targets);
|
||||
|
||||
13
test/programs/annotate.flan
Normal file
13
test/programs/annotate.flan
Normal file
@ -0,0 +1,13 @@
|
||||
;;;; A function whose forms nest and loop, for the source annotation of its
|
||||
;;;; IR, its x86 listing and its disassembly: test_dev.ml reads the forms back
|
||||
;;;; out of each and checks they come in the order they are written here.
|
||||
|
||||
(defn wind [n i32] i32
|
||||
(let [acc 0]
|
||||
(dotimes [i n]
|
||||
(set acc (+ acc (* i 3))))
|
||||
acc))
|
||||
|
||||
(defn main [] i32
|
||||
(print (wind 10))
|
||||
0)
|
||||
179
test/test_dev.ml
179
test/test_dev.ml
@ -3505,6 +3505,185 @@ let () =
|
||||
end;
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ dsock; dout ];
|
||||
|
||||
(* ── Source annotation ─────────────────────────────────────────── *)
|
||||
|
||||
(* The IR, the x86 listing and both disassemblies are headed by the Flan
|
||||
forms they came from. Each is checked for the forms of one function in
|
||||
the order they are written, and the objects are checked to be the same
|
||||
with and without the headings, since the whole licence for writing them
|
||||
is that nothing but a reader sees them. *)
|
||||
let in_order text needles =
|
||||
let rec go from = function
|
||||
| [] -> true
|
||||
| n :: rest ->
|
||||
let ln = String.length n and lt = String.length text in
|
||||
let rec find i =
|
||||
if i + ln > lt then None
|
||||
else if String.sub text i ln = n then Some (i + ln)
|
||||
else find (i + 1)
|
||||
in
|
||||
(match find from with Some i -> go i rest | None -> false)
|
||||
in
|
||||
go 0 needles
|
||||
in
|
||||
let forms =
|
||||
[ "(let [acc 0]"; "(dotimes [i n]"; "(set acc (+ acc (* i 3)))";
|
||||
"(+ acc (* i 3))"; "(* i 3)" ]
|
||||
in
|
||||
let p = Test_support.checked "programs/annotate.flan" in
|
||||
let ll = Emit.program ~annotate:true p and ll0 = Emit.program p in
|
||||
(match Dev.ir_of ~ir:ll "wind" with
|
||||
| None -> fail "annotate: no define for wind in the annotated IR"
|
||||
| Some body ->
|
||||
if not (in_order body forms) then
|
||||
fail "annotate: the IR's headings are not the forms in order:\n%s" body;
|
||||
if not (contains_sub body "annotate.flan:6:3") then
|
||||
fail "annotate: the IR's heading does not name the position:\n%s" body);
|
||||
if contains_sub ll0 "annotate.flan:" then
|
||||
fail "annotate: an IR nobody asked to annotate carries headings";
|
||||
let asm = X86.program ~checks:true ~annotate:true p
|
||||
and asm0 = X86.program ~checks:true p in
|
||||
let map = Dev.x86_source ~asm "wind" in
|
||||
if not (in_order (String.concat "\n" (List.map snd map)) forms) then
|
||||
fail "annotate: the x86 source map is not the forms in order: %s"
|
||||
(String.concat " | " (List.map snd map));
|
||||
let have prog = Sys.command ("command -v " ^ prog ^ " > /dev/null 2>&1") = 0 in
|
||||
if not (have "llc" && have "as" && have "objcopy" && have "objdump") then
|
||||
print_endline "dev: annotation parity skipped (no llc, as, objcopy or objdump)"
|
||||
else begin
|
||||
let write path text =
|
||||
let oc = open_out_bin path in
|
||||
output_string oc text;
|
||||
close_out oc
|
||||
in
|
||||
let quiet cmd = Sys.command (cmd ^ " > /dev/null 2>&1") = 0 in
|
||||
let section obj sec =
|
||||
let bin = obj ^ sec ^ ".bin" in
|
||||
if quiet
|
||||
(Printf.sprintf "objcopy -O binary --only-section=%s %s %s" sec
|
||||
(Filename.quote obj) (Filename.quote bin))
|
||||
then begin
|
||||
let text = Build.read_file bin in
|
||||
(try Sys.remove bin with Sys_error _ -> ());
|
||||
Some text
|
||||
end
|
||||
else None
|
||||
in
|
||||
let same what a b =
|
||||
List.iter
|
||||
(fun sec ->
|
||||
if section a sec <> section b sec then
|
||||
fail "annotate: %s: the %s section differs with the headings" what sec)
|
||||
[ ".text"; ".data"; ".rodata"; ".data.rel.ro" ]
|
||||
in
|
||||
let al = tmp "annot-a.ll" and bl = tmp "annot-b.ll"
|
||||
and ao = tmp "annot-a.o" and bo = tmp "annot-b.o"
|
||||
and as_ = tmp "annot-a.s" and bs = tmp "annot-b.s"
|
||||
and aso = tmp "annot-as.o" and bso = tmp "annot-bs.o"
|
||||
and dl = tmp "annot-d.ll" and dob = tmp "annot-d.o" in
|
||||
write al ll; write bl ll0; write as_ asm; write bs asm0;
|
||||
let llc src obj =
|
||||
quiet (Printf.sprintf "llc -O0 -filetype=obj -relocation-model=pic %s -o %s"
|
||||
(Filename.quote src) (Filename.quote obj))
|
||||
and gas src obj =
|
||||
quiet (Printf.sprintf "as --64 %s -o %s" (Filename.quote src)
|
||||
(Filename.quote obj))
|
||||
in
|
||||
if not (llc al ao && llc bl bo) then fail "annotate: llc refused the IR"
|
||||
else same "llc -O0" ao bo;
|
||||
if not (gas as_ aso && gas bs bso) then fail "annotate: as refused the listing"
|
||||
else begin
|
||||
same "the x86 backend" aso bso;
|
||||
(* The daemon's view of the same object: every offset in the map is an
|
||||
instruction's, and the forms land in the listing in order. *)
|
||||
match Dev.asm_of ~source:map ~obj:aso "wind" with
|
||||
| Error m -> fail "annotate: disassembling the x86 object: %s" m
|
||||
| Ok (text, placed) ->
|
||||
if not placed then fail "annotate: no source placed in the x86 listing";
|
||||
if not (in_order text forms) then
|
||||
fail "annotate: the x86 disassembly is not headed in order:\n%s" text;
|
||||
List.iter
|
||||
(fun (off, _) ->
|
||||
if not (contains_sub text (Printf.sprintf " %04x " off)) then
|
||||
fail "annotate: the map's offset %x is no instruction's:\n%s" off text)
|
||||
map
|
||||
end;
|
||||
(* An LLVM build has a line table under --debug, and the form each line
|
||||
is headed with is read out of the annotated IR. *)
|
||||
let dir = Emit.program ~debug:true ~annotate:true p in
|
||||
write dl dir;
|
||||
if not (llc dl dob) then fail "annotate: llc refused the debug IR"
|
||||
else begin
|
||||
match Dev.asm_of ~ir:dir ~obj:dob "wind" with
|
||||
| Error m -> fail "annotate: disassembling the LLVM object: %s" m
|
||||
| Ok (text, placed) ->
|
||||
if not placed then fail "annotate: no line table read from a --debug object";
|
||||
if not (in_order text
|
||||
[ "(let [acc 0]"; "(dotimes [i n]"; "(set acc (+ acc (* i 3)))" ])
|
||||
then fail "annotate: the LLVM disassembly is not headed in order:\n%s" text
|
||||
end;
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||
[ al; bl; ao; bo; as_; bs; aso; bso; dl; dob ]
|
||||
end;
|
||||
|
||||
(* And through the daemon, on the backend it takes unasked: the host's own
|
||||
body, and a body delivered from a buffer that is not on disk. *)
|
||||
if have "objdump" then begin
|
||||
let xsock = tmp "annot.sock" and xout = tmp "annot.out" in
|
||||
(try Sys.remove xsock with Sys_error _ -> ());
|
||||
let xfd = Unix.openfile xout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600 in
|
||||
let xpid =
|
||||
Unix.create_process flan
|
||||
[| flan; "dev"; "programs/dev-repl.flan"; "-s"; xsock |]
|
||||
Unix.stdin xfd Unix.stderr
|
||||
in
|
||||
Unix.close xfd;
|
||||
if not (listening ~pid:xpid xsock) then begin
|
||||
fail "the annotation daemon %s" !listen_why;
|
||||
(try Unix.kill xpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end
|
||||
else begin
|
||||
let c = connect xsock in
|
||||
let text r = Option.value ~default:"" (Wire.string_field r "text") in
|
||||
let said r = Option.value ~default:"" (Wire.string_field r "message") in
|
||||
let r = request c "(:op \"disassemble\" :name \"step\" :form \"asm\")" in
|
||||
if status r <> "ok" then fail "annotate: the host's step: %s" (said r)
|
||||
else begin
|
||||
if not (in_order (text r) [ "(set ticks (+ ticks 1))"; "ret" ]) then
|
||||
fail "annotate: the host's step is not headed by its source:\n%s" (text r);
|
||||
if Wire.string_field r "note" <> None then
|
||||
fail "annotate: an annotated listing still carries a note"
|
||||
end;
|
||||
let r =
|
||||
request c
|
||||
"(:op \"eval\" :code \"\n\n(defn wind [n i32] i32\n (let [acc 0]\n (dotimes [i n]\n (set acc (+ acc (* i 3))))\n acc))\" :file \"/tmp/flan-annotate-unsaved.flan\")"
|
||||
in
|
||||
if status r <> "ok" then fail "annotate: delivering wind: %s" (said r)
|
||||
else begin
|
||||
let r = request c "(:op \"disassemble\" :name \"wind\" :form \"asm\")" in
|
||||
if status r <> "ok" then fail "annotate: the delivered wind: %s" (said r)
|
||||
else if not (in_order (text r) forms) then
|
||||
fail "annotate: the delivered wind is not headed in order:\n%s" (text r)
|
||||
else if not (contains_sub (text r) "flan-annotate-unsaved.flan:4:3") then
|
||||
fail "annotate: the delivered wind's positions are not the buffer's:\n%s"
|
||||
(text r)
|
||||
end;
|
||||
ignore (request c "(:op \"close\")");
|
||||
Unix.close c;
|
||||
if not
|
||||
(await ~ms:5000 (fun () ->
|
||||
match Unix.waitpid [ Unix.WNOHANG ] xpid with
|
||||
| 0, _ -> false
|
||||
| _ -> true
|
||||
| exception Unix.Unix_error _ -> true))
|
||||
then begin
|
||||
(try Unix.kill xpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] xpid) with Unix.Unix_error _ -> ())
|
||||
end
|
||||
end;
|
||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) [ xsock; xout ]
|
||||
end;
|
||||
|
||||
(* ── A location that survives an evaluation that did not land ───── *)
|
||||
|
||||
(* [Session.eval] replaces the checked program the moment a form checks,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user