Eight hand-written .fln programs run the same on both backends and through a conversion to parens and back
This commit is contained in:
parent
8fe8062a0c
commit
7e48f1eaf5
11
lib/check.ml
11
lib/check.ml
@ -6200,6 +6200,13 @@ and check_fn ctx ~want ?gen loc (params : string list) body =
|
|||||||
| Some other when other <> Types.Never ->
|
| Some other when other <> Types.Never ->
|
||||||
fail loc "expected %s, found an fn" (Types.to_string other)
|
fail loc "expected %s, found an fn" (Types.to_string other)
|
||||||
| _ ->
|
| _ ->
|
||||||
|
if fln_source loc then
|
||||||
|
fail loc
|
||||||
|
"nothing here says what this fn's parameters are — an fn takes \
|
||||||
|
its types from the position it is written in. Pass it where a \
|
||||||
|
Fn(T, ...) -> R is expected, or name the type where it is \
|
||||||
|
bound: let f: Fn(T, ...) -> R = fn(...)"
|
||||||
|
else
|
||||||
fail loc
|
fail loc
|
||||||
"nothing here says what this fn's parameters are — an fn takes \
|
"nothing here says what this fn's parameters are — an fn takes \
|
||||||
its types from the position it is written in. Write it as an \
|
its types from the position it is written in. Write it as an \
|
||||||
@ -10701,8 +10708,10 @@ and named_call ?(qualified = false) ctx ~want loc name args =
|
|||||||
fail loc
|
fail loc
|
||||||
"a pattern cannot destructure %s — a slice's length is not known \
|
"a pattern cannot destructure %s — a slice's length is not known \
|
||||||
until the program runs, so nothing here can check it has %Ld \
|
until the program runs, so nothing here can check it has %Ld \
|
||||||
element%s. Use (at s i) and test (length s) yourself"
|
element%s. Use %s and test %s yourself"
|
||||||
(Types.to_string target.Tast.ty) n (plural n)
|
(Types.to_string target.Tast.ty) n (plural n)
|
||||||
|
(if fln_source loc then "s[i]" else "(at s i)")
|
||||||
|
(if fln_source loc then "length(s)" else "(length s)")
|
||||||
| other ->
|
| other ->
|
||||||
fail loc
|
fail loc
|
||||||
"%s is not a fixed array, so [a b ...] cannot destructure it"
|
"%s is not a fixed array, so [a b ...] cannot destructure it"
|
||||||
|
|||||||
@ -398,6 +398,10 @@ and list _f h args =
|
|||||||
&& (match t.v with
|
&& (match t.v with
|
||||||
| Form.Byte _ -> false
|
| Form.Byte _ -> false
|
||||||
| Form.Sym x -> name_ok x && not (String.contains x '.') && not (R.capitalised x)
|
| Form.Sym x -> name_ok x && not (String.contains x '.') && not (R.capitalised x)
|
||||||
|
(* A field of a field chains: [w.x.count]. *)
|
||||||
|
| Form.List [ { v = Form.Sym f; _ }; _ ]
|
||||||
|
when String.length f > 1 && f.[0] = '.' && tt <> "" && tt.[0] <> '.' ->
|
||||||
|
String.length tt > 0 && not (String.contains tt ' ')
|
||||||
| _ ->
|
| _ ->
|
||||||
let c = tt.[String.length tt - 1] in
|
let c = tt.[String.length tt - 1] in
|
||||||
c = ')' || c = ']' || c = '}' || c = '"')
|
c = ')' || c = ']' || c = '}' || c = '"')
|
||||||
@ -760,8 +764,15 @@ and sugar n (f : Form.t) : string list option =
|
|||||||
| Form.List ({ v = Form.Sym h; _ } :: _) -> not (List.mem h sugar_heads)
|
| Form.List ({ v = Form.Sym h; _ } :: _) -> not (List.mem h sugar_heads)
|
||||||
| _ -> true
|
| _ -> true
|
||||||
in
|
in
|
||||||
|
(* An else that is another if with an else chains on the line:
|
||||||
|
[if a then x else if b then y else z]. *)
|
||||||
|
let rec chain (x : Form.t) =
|
||||||
|
match x.v with
|
||||||
|
| Form.List [ { v = Form.Sym "if"; _ }; _; a; b ] -> simple a && chain b
|
||||||
|
| _ -> simple x
|
||||||
|
in
|
||||||
let line = i ^ fst (expr f) in
|
let line = i ^ fst (expr f) in
|
||||||
if simple a && simple b && String.length line <= width && not (!inside f)
|
if simple a && chain b && String.length line <= width && not (!inside f)
|
||||||
then Some [ line ]
|
then Some [ line ]
|
||||||
else
|
else
|
||||||
Some
|
Some
|
||||||
|
|||||||
@ -812,7 +812,21 @@ and items p closer open_loc ~what =
|
|||||||
| EOF -> unclosed p opener open_loc
|
| EOF -> unclosed p opener open_loc
|
||||||
| _ ->
|
| _ ->
|
||||||
let n = peek p in
|
let n = peek p in
|
||||||
if starts_value n.tok && n.sp && not (negative_literal n.tok) then
|
let block_lambda =
|
||||||
|
match e.v with
|
||||||
|
| Form.List ({ v = Form.Sym "fn"; _ } :: ps) ->
|
||||||
|
List.for_all (fun (a : Form.t) -> match a.v with Form.Sym _ -> true | _ -> false) ps
|
||||||
|
&& n.loc.Loc.line > e.loc.Loc.eline
|
||||||
|
| _ -> false
|
||||||
|
in
|
||||||
|
if block_lambda then
|
||||||
|
failk "lambda-block-in-brackets" n.loc
|
||||||
|
"a lambda's block cannot go inside brackets, where a line break \
|
||||||
|
is only a space. Name it first, with the block under it:\n\n\
|
||||||
|
\ let f = %s\n ...\n\n\
|
||||||
|
and pass f, or write it on one line: %s = value"
|
||||||
|
(text_of e) (text_of e)
|
||||||
|
else if starts_value n.tok && n.sp && not (negative_literal n.tok) then
|
||||||
failk "missing-comma" n.loc
|
failk "missing-comma" n.loc
|
||||||
"%s follows %s with no comma between them. Separate %s with \
|
"%s follows %s with no comma between them. Separate %s with \
|
||||||
commas: f(a, b)"
|
commas: f(a, b)"
|
||||||
@ -897,6 +911,29 @@ let rec ty p : Form.t =
|
|||||||
as a call does not. *)
|
as a call does not. *)
|
||||||
type st = { p : p; mutable lets : Form.t list }
|
type st = { p : p; mutable lets : Form.t list }
|
||||||
|
|
||||||
|
(* Whether the code line before [t] is a one-line [if c then a] with no
|
||||||
|
else: an else under it reads as written for that if, and is not. *)
|
||||||
|
let one_line_if_above p (t : token) =
|
||||||
|
let layout = function NEWLINE | INDENT | DEDENT -> true | _ -> false in
|
||||||
|
let rec prev j =
|
||||||
|
if j < 0 then None
|
||||||
|
else
|
||||||
|
let u = p.toks.(j) in
|
||||||
|
if u.loc.Loc.line < t.loc.Loc.line && not (layout u.tok) then Some u.loc.Loc.line
|
||||||
|
else prev (j - 1)
|
||||||
|
in
|
||||||
|
match prev (p.i - 1) with
|
||||||
|
| None -> false
|
||||||
|
| Some l ->
|
||||||
|
let rec line j acc =
|
||||||
|
if j < 0 || p.toks.(j).loc.Loc.line < l then acc
|
||||||
|
else line (j - 1) (if layout p.toks.(j).tok || p.toks.(j).loc.Loc.line > l then acc
|
||||||
|
else p.toks.(j).tok :: acc)
|
||||||
|
in
|
||||||
|
(match line (p.i - 1) [] with
|
||||||
|
| NAME "if" :: rest -> List.mem (NAME "then") rest && not (List.mem (NAME "else") rest)
|
||||||
|
| _ -> false)
|
||||||
|
|
||||||
(* A block of several lines is a [do] spanning its lines, from the first
|
(* A block of several lines is a [do] spanning its lines, from the first
|
||||||
statement to the end of the last — not from the header above it, which is
|
statement to the end of the last — not from the header above it, which is
|
||||||
another form's. *)
|
another form's. *)
|
||||||
@ -1132,6 +1169,14 @@ and stmt (s : st) : Form.t =
|
|||||||
let t = peek p in
|
let t = peek p in
|
||||||
match t.tok with
|
match t.tok with
|
||||||
| NAME w when header_follow p w -> header s w
|
| NAME w when header_follow p w -> header s w
|
||||||
|
| NAME (("else" | "elif") as w) when one_line_if_above p t ->
|
||||||
|
failk "orphan-else" t.loc
|
||||||
|
"the if above is a one-line if, which ends with its line, so this %s \
|
||||||
|
has no if to belong to. Keep the one-line form on one line:\n\n\
|
||||||
|
\ if c then a else b\n\n\
|
||||||
|
or give each branch a block:\n\n\
|
||||||
|
\ if c\n a\n else\n b"
|
||||||
|
w
|
||||||
| NAME (("else" | "elif") as w) ->
|
| NAME (("else" | "elif") as w) ->
|
||||||
failk "orphan-else" t.loc
|
failk "orphan-else" t.loc
|
||||||
"%s is not under an if at this column. It goes at the same column as \
|
"%s is not under an if at this column. It goes at the same column as \
|
||||||
|
|||||||
91
test/syntax/handwritten/traffic.fln
Normal file
91
test/syntax/handwritten/traffic.fln
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
; A traffic light at a crossing, driven by a clock and a pedestrian button.
|
||||||
|
; Each state carries how long it has left; a fault drops the light to
|
||||||
|
; flashing, and a supervisor decides whether to reset it.
|
||||||
|
|
||||||
|
data Light
|
||||||
|
Green(left: i32)
|
||||||
|
Yellow(left: i32, walk: bool)
|
||||||
|
Red(left: i32, walk: bool)
|
||||||
|
Flashing()
|
||||||
|
|
||||||
|
struct Fault
|
||||||
|
tick: i32
|
||||||
|
|
||||||
|
const green-time = 4
|
||||||
|
const yellow-time = 1
|
||||||
|
const red-time = 3
|
||||||
|
|
||||||
|
fn next(l: Light, pressed: bool) -> Light
|
||||||
|
match l
|
||||||
|
Green(left) ->
|
||||||
|
if left > 1 and not pressed
|
||||||
|
Light.Green{.left left - 1}
|
||||||
|
else
|
||||||
|
Light.Yellow{.left yellow-time, .walk pressed}
|
||||||
|
Yellow(left, walk) ->
|
||||||
|
if left > 1
|
||||||
|
Light.Yellow{.left left - 1, .walk walk}
|
||||||
|
else
|
||||||
|
Light.Red{.left red-time, .walk walk}
|
||||||
|
Red(left, walk) ->
|
||||||
|
if left > 1 then Light.Red{.left left - 1, .walk walk} else Light.Green{.left green-time}
|
||||||
|
Flashing -> Light.Flashing{}
|
||||||
|
|
||||||
|
fn show(l: Light) -> string
|
||||||
|
match l
|
||||||
|
Green(_) -> "G"
|
||||||
|
Yellow(_, _) -> "Y"
|
||||||
|
Red(_, walk) -> if walk then "W" else "R"
|
||||||
|
Flashing -> "*"
|
||||||
|
|
||||||
|
; Runs the light for ticks steps; a fault at fault-at is signalled, and
|
||||||
|
; whoever handles it may reset the light to red.
|
||||||
|
fn run(ticks: i32, presses: [const i32], fault-at: i32) -> ()
|
||||||
|
let l = Light.Red{.left 1, .walk false}
|
||||||
|
let p = 0
|
||||||
|
let t = 0
|
||||||
|
until :clock t >= ticks
|
||||||
|
let pressed = p < length(presses)
|
||||||
|
and presses[p] == t
|
||||||
|
if pressed
|
||||||
|
++(p)
|
||||||
|
if t == fault-at
|
||||||
|
l = restart-case
|
||||||
|
error(Fault{.tick t})
|
||||||
|
l
|
||||||
|
restart reset()
|
||||||
|
:report
|
||||||
|
"Put the light back to red and carry on"
|
||||||
|
Light.Red{.left red-time, .walk false}
|
||||||
|
restart flash()
|
||||||
|
Light.Flashing{}
|
||||||
|
match l
|
||||||
|
Flashing ->
|
||||||
|
print(show(l))
|
||||||
|
break :clock
|
||||||
|
_ -> print(show(l))
|
||||||
|
l = next(l, pressed)
|
||||||
|
t += 1
|
||||||
|
println("")
|
||||||
|
|
||||||
|
fn main() -> i32
|
||||||
|
let none = [-1]
|
||||||
|
let two = [1 9]
|
||||||
|
run(12, slice(none), -1)
|
||||||
|
run(12, slice(two), -1)
|
||||||
|
handler-bind
|
||||||
|
run(12, slice(none), 5)
|
||||||
|
on Fault(f)
|
||||||
|
invoke-restart('reset)
|
||||||
|
handler-bind
|
||||||
|
run(12, slice(none), 5)
|
||||||
|
on Fault(f)
|
||||||
|
invoke-restart('flash)
|
||||||
|
println:
|
||||||
|
handler-case
|
||||||
|
run(12, slice(none), 2)
|
||||||
|
"no fault"
|
||||||
|
on Fault(f)
|
||||||
|
println("")
|
||||||
|
"fault at tick"
|
||||||
|
0
|
||||||
6
test/syntax/handwritten/traffic.out
Normal file
6
test/syntax/handwritten/traffic.out
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
RGGGGYRRRGGG
|
||||||
|
RGYWWWGGGGYW
|
||||||
|
RGGGGRRRGGGG
|
||||||
|
RGGGG*
|
||||||
|
RG
|
||||||
|
fault at tick
|
||||||
84
test/syntax/handwritten/words.fln
Normal file
84
test/syntax/handwritten/words.fln
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
; Word statistics over a paragraph: a frequency table, the longest words,
|
||||||
|
; and a grade for how varied the vocabulary is.
|
||||||
|
|
||||||
|
enum Grade
|
||||||
|
poor = 1
|
||||||
|
fair = 2
|
||||||
|
rich = 3
|
||||||
|
|
||||||
|
struct Count
|
||||||
|
word: [const u8]
|
||||||
|
n: i32
|
||||||
|
|
||||||
|
fn letter?(c: u8) -> bool
|
||||||
|
(c >= \a and c <= \z)
|
||||||
|
or (c >= \A and c <= \Z)
|
||||||
|
or c == \'
|
||||||
|
|
||||||
|
; The words of text, lowercased, in order.
|
||||||
|
fn words(text: [const u8]) -> Vec([const u8])
|
||||||
|
let out = vec-new([const u8])
|
||||||
|
let lower = to-lower(text)
|
||||||
|
let i = 0
|
||||||
|
let n = length(lower)
|
||||||
|
while :scan i < n
|
||||||
|
until i >= n or letter?(lower[i])
|
||||||
|
i += 1
|
||||||
|
if i >= n
|
||||||
|
break :scan
|
||||||
|
let start = i
|
||||||
|
while i < n and letter?(lower[i])
|
||||||
|
i += 1
|
||||||
|
push(out, slice(lower, start, i))
|
||||||
|
out
|
||||||
|
|
||||||
|
fn tally(ws: [[const u8]]) -> Vec(Count)
|
||||||
|
let seen = map-new(string, i32)
|
||||||
|
defer free(seen)
|
||||||
|
let counts = vec-new(Count)
|
||||||
|
for i in range(length(ws))
|
||||||
|
match get(seen, string(ws[i]))
|
||||||
|
Some(k) -> counts[k].n += 1
|
||||||
|
None ->
|
||||||
|
put(seen, string(ws[i]), i32(length(counts)))
|
||||||
|
push(counts, Count{.word ws[i], .n 1})
|
||||||
|
counts
|
||||||
|
|
||||||
|
fn grade(distinct: i32, total: i32) -> Grade
|
||||||
|
let ratio = distinct * 10 / max(total, 1)
|
||||||
|
if ratio >= 7 then :rich else if ratio >= 4 then :fair else :poor
|
||||||
|
|
||||||
|
fn describe(g: Grade) -> string
|
||||||
|
match i32(g)
|
||||||
|
1 -> "repetitive"
|
||||||
|
2 -> "ordinary"
|
||||||
|
_ -> "varied"
|
||||||
|
|
||||||
|
fn main() -> i32
|
||||||
|
let text = "The cat saw the dog. The dog didn't see the cat, but the bird saw both!"
|
||||||
|
let ws = words(bytes-view(text))
|
||||||
|
let counts = tally(slice(ws))
|
||||||
|
let by-count: Fn(Count, Count) -> bool = fn(a, b)
|
||||||
|
if a.n != b.n
|
||||||
|
return a.n > b.n
|
||||||
|
bytes<?(a.word, b.word)
|
||||||
|
sort-by(slice(counts), by-count)
|
||||||
|
for i in range(3)
|
||||||
|
let {.word .n} = counts[i]
|
||||||
|
println(string(word), n)
|
||||||
|
let top: [3 i32] = [counts[0].n counts[1].n counts[2].n]
|
||||||
|
let [most & others] = top
|
||||||
|
println("most frequent seen", most, "times, then", others)
|
||||||
|
let total = i32(length(ws))
|
||||||
|
let distinct = i32(length(counts))
|
||||||
|
let g = grade(distinct, total)
|
||||||
|
println(distinct, "of", total, "distinct:", describe(g))
|
||||||
|
let longest = reduce(slice(ws), slice(ws[0], 0, 0), fn(a, b) =
|
||||||
|
if length(b) > length(a) then b else a)
|
||||||
|
println("longest", string(longest))
|
||||||
|
let short = 0 < length(longest) < 5
|
||||||
|
match short
|
||||||
|
true -> println("short words only")
|
||||||
|
false -> println("some long words")
|
||||||
|
println("three distinct counts?", !=(counts[0].n, counts[1].n, counts[2].n))
|
||||||
|
0
|
||||||
8
test/syntax/handwritten/words.out
Normal file
8
test/syntax/handwritten/words.out
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
the 5
|
||||||
|
cat 2
|
||||||
|
dog 2
|
||||||
|
most frequent seen 5 times, then [2 2]
|
||||||
|
9 of 16 distinct: ordinary
|
||||||
|
longest didn't
|
||||||
|
some long words
|
||||||
|
three distinct counts? false
|
||||||
@ -918,7 +918,7 @@ let () =
|
|||||||
|
|
||||||
(* ── Both directions of an import, on both backends ────────────────── *)
|
(* ── Both directions of an import, on both backends ────────────────── *)
|
||||||
|
|
||||||
let run_both path want =
|
let run_both ?(backends = [ false; true ]) path want =
|
||||||
List.iter
|
List.iter
|
||||||
(fun x86 ->
|
(fun x86 ->
|
||||||
let exe =
|
let exe =
|
||||||
@ -939,7 +939,7 @@ let run_both path want =
|
|||||||
if code <> 0 || text <> want then
|
if code <> 0 || text <> want then
|
||||||
fail "%s%s printed %S and exited %d, wanted %S" path
|
fail "%s%s printed %S and exited %d, wanted %S" path
|
||||||
(if x86 then " --x86" else "") text code want)
|
(if x86 then " --x86" else "") text code want)
|
||||||
[ false; true ]
|
backends
|
||||||
|
|
||||||
(* A program and its conversion print the same: the flat lets, the renames
|
(* A program and its conversion print the same: the flat lets, the renames
|
||||||
and the macro bodies they rest on keep what each name means. *)
|
and the macro bodies they rest on keep what each name means. *)
|
||||||
@ -972,6 +972,66 @@ let run_converted path =
|
|||||||
| ((c, a), (d, b)) ->
|
| ((c, a), (d, b)) ->
|
||||||
fail "%s printed %S (exit %d), and converted %S (exit %d)" path a c b d
|
fail "%s printed %S (exit %d), and converted %S (exit %d)" path a c b d
|
||||||
|
|
||||||
|
(* ── Programs written by hand in the indented syntax ─────────────────── *)
|
||||||
|
|
||||||
|
(* Each [syntax/handwritten/x.fln] prints [x.out] on both backends; its
|
||||||
|
conversion to parens reads back to the same forms, with every comment,
|
||||||
|
and converts back to indented text that reads to them again; and the
|
||||||
|
converted .flan builds and prints the same. *)
|
||||||
|
let handwritten () =
|
||||||
|
let dir = "syntax/handwritten" in
|
||||||
|
Sys.readdir dir |> Array.to_list
|
||||||
|
|> List.filter (fun f -> Filename.check_suffix f ".fln")
|
||||||
|
|> List.sort compare
|
||||||
|
|> List.map (Filename.concat dir)
|
||||||
|
|
||||||
|
let converts_back path =
|
||||||
|
let src = In_channel.with_open_bin path In_channel.input_all in
|
||||||
|
let forms = Source.read_file path in
|
||||||
|
let norm_all fs =
|
||||||
|
macros := Body_macros.table ~file:path fs;
|
||||||
|
List.map norm fs
|
||||||
|
in
|
||||||
|
let want = norm_all forms in
|
||||||
|
let paren = Paren_printer.program ~source:src forms in
|
||||||
|
match Reader.read_all ~file:path paren with
|
||||||
|
| exception e -> fail "%s to parens: %s" path (diag_text e)
|
||||||
|
| again ->
|
||||||
|
if not (same_forms want (norm_all again)) then
|
||||||
|
fail "%s to parens: %s" path (describe_diff want (norm_all again))
|
||||||
|
else if comment_texts paren <> comment_texts src then
|
||||||
|
fail "%s to parens: the comments did not all come through" path
|
||||||
|
else begin
|
||||||
|
let fln = Indent_printer.program ~source:paren ~macros:!macros again in
|
||||||
|
(match Indent_reader.read_all ~file:path fln with
|
||||||
|
| exception e -> fail "%s back to indented: %s" path (diag_text e)
|
||||||
|
| back ->
|
||||||
|
if not (same_forms want (norm_all back)) then
|
||||||
|
fail "%s back to indented: %s" path (describe_diff want (norm_all back)));
|
||||||
|
(* Beside the original, so its imports resolve the same way. *)
|
||||||
|
let flan = Filename.concat (Filename.dirname path)
|
||||||
|
(Printf.sprintf ".conv-%d-%s.flan" (Unix.getpid ())
|
||||||
|
(Filename.remove_extension (Filename.basename path))) in
|
||||||
|
Out_channel.with_open_bin flan (fun oc -> output_string oc paren);
|
||||||
|
Fun.protect ~finally:(fun () -> try Sys.remove flan with Sys_error _ -> ())
|
||||||
|
(fun () -> run_both ~backends:[ false ] flan
|
||||||
|
(In_channel.with_open_bin (Filename.remove_extension path ^ ".out")
|
||||||
|
In_channel.input_all))
|
||||||
|
end
|
||||||
|
|
||||||
|
let () =
|
||||||
|
List.iter
|
||||||
|
(fun p -> try converts_back p with e -> fail "%s: %s" p (diag_text e))
|
||||||
|
(List.filter (fun _ -> Test_support.have "clang") (handwritten ()));
|
||||||
|
if List.length (handwritten ()) < 8 then
|
||||||
|
fail "only %d hand-written programs" (List.length (handwritten ()));
|
||||||
|
if Test_support.have "clang" then
|
||||||
|
List.iter
|
||||||
|
(fun p ->
|
||||||
|
run_both p (In_channel.with_open_bin (Filename.remove_extension p ^ ".out")
|
||||||
|
In_channel.input_all))
|
||||||
|
(handwritten ())
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
if Test_support.have "clang" then begin
|
if Test_support.have "clang" then begin
|
||||||
List.iter run_converted
|
List.iter run_converted
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user