caml-list - the Caml user's mailing list
 help / color / mirror / Atom feed
* [Caml-list] record labels of record scope using camlp4
@ 2002-01-14  7:28 Jeff Henrikson
  2002-01-14 10:01 ` Daniel de Rauglaudre
  2002-01-14 12:15 ` Alain Frisch
  0 siblings, 2 replies; 6+ messages in thread
From: Jeff Henrikson @ 2002-01-14  7:28 UTC (permalink / raw)
  To: caml-list

It seems like a lot of people dislike the fact that record labels have module scope instead of record scope.  Looking at the
identifier style in the ocaml source we see the symmetry behind this design choice:

<from parsing/parsetree.mli>
(* Type expressions for the core language *)

type core_type =
  { ptyp_desc: core_type_desc;
    ptyp_loc: Location.t }

and core_type_desc =
    Ptyp_any
  | Ptyp_var of string
  | Ptyp_arrow of label * core_type * core_type
  | Ptyp_tuple of core_type list
  | Ptyp_constr of Longident.t * core_type list
  | Ptyp_object of core_field_type list
  | Ptyp_class of Longident.t * core_type list * label list
  | Ptyp_alias of core_type * string
  | Ptyp_variant of row_field list * bool * label list option

and core_field_type =
  { pfield_desc: core_field_desc;
    pfield_loc: Location.t }


I.e. an abbreviated type always prefixes constructors or labels.  But I find this annoying when records get long, since to type a
pattern for a record with n entries requires specifying the type n times!  It is even tempting to accept the incentive of creating
short type abbreviations to resist typing a long identifier n times.  And eliminating the type from the labels entirely doesn't
work if you have lots of fields called generic things like "name" and "address".

On the other hand, I wouldn't have a huge problem with this if it were all I had to deal with was:

 var.Type_label

*So* I took the chance to learn some camlp4 and wrote a macro that implements declarations and patterns having record-local scope
like this:

  type person = LOCAL {name:string; addr: string};;
  let p = Person{name="Joe";addr="1 Broadway Ave"};;
  match p with
    Person{name=n;addr=a} -> (n,a);;

It does this by prepending the name of the type onto the front of the identifiers and then automatically adding them back on the
patterns, that is:

  type person = { person_name : string; person_addr : string; }

so of course you are still stuck with:

  p.person_name

but it seems a small price to pay.  The extension properly handles recursive declarations, eg:

type person = LOCAL {name:string; addr:string; friends:plist}
and plist = Plist_person of (person * plist) | List_End;;


_The bad news_

Okay, so there are some problems.

First bad news: the code runs great in the toplevel, but that's because camlp4 extends grammars interactively there.  Apparently in
batch compilation, any EXTEND construct only takes effect at the end of the file, not at the end of a statement.  I remember
reading this in the manual somewhere, though I don't seem to be able to find it now.  And every time you use the LOCAL syntax
above, it defines a new macro for the constructor.  So the macro won't be visible until the file is done parsing.

A workaround might be to stuff all typedefs in one file and then #load them into another.  I'd have to think about the
consequences.  It smells funny.

Second bad news: the scoping isn't quite right.  The types will be in module scope, but their constructor macros will be global.
The good news is that the macros themselves don't know about the members of the types at all.  They just syntactically go pasting
stuff onto them.  So concievably if you had two records with the same names in different modules, nothing bad would happen.  Again,
it smells funny.


_To Do_

Besides working out the practical problems above, I'd like to see more symmetry between function labels and record labels, eg
default values.  Labels are great if you are creating an API to be called, but they do nothing to help if you are creating a
callback API.  As in "Here are a bunch of parameters, you don't have to use/know them all if you don't want."  Adding default
values seems feasible, but it would exacerbate the macro scoping problem.

Here's the source below.  You can evaluate it in the toplevel.  Have fun.


Jeff Henrikson


PS-

> PS> I guess I can understand Xavier, sometimes some people complain
> about so-called bugs which are in facts features, and for features
> such as the ``cannot use record labels as I would have wished to'' it
> is just too often.

Apparently camlp4 has, or nearly has, the power to supersede such quibbling.  :-)


----------------------------------------------------------------


#load "camlp4o.cma";;    (* toploop only *)

#load "q_MLast.cmo";;
#load "pa_extend.cmo";;

open Pcaml;;

let loc = (0,0);;        (* toploop only *)

let prepend_id name labels =
  let helper2 v =
    match v with
    <:patt<$lid:x$>> ->
      let y = name ^ "_" ^ x in
      <:patt<$lid:y$>>
  | _ -> v in
  let helper1 = fun
    (patt,expr) -> (helper2 patt,expr) in
  List.map helper1 labels;;



let make_record_constructor name =
  let lname = String.uncapitalize name in
  let uname = String.capitalize name in
  let lbl_expr_list =
    (Obj.magic (Grammar.Entry.find expr "lbl_expr_list") :
       (MLast.patt * MLast.expr) list Grammar.Entry.e) in
  let lbl_patt_list =
    (Obj.magic (Grammar.Entry.find patt "lbl_patt_list") :
       (MLast.patt * MLast.patt) list Grammar.Entry.e) in
  EXTEND
    expr: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_expr_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:expr<{$list:memb0$}>>) ] ];
    patt: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_patt_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:patt<{$list:memb0$}>>) ] ];
  END;
;;

(* test:
make_record_constructor "bogus";;

type bogus = {bogus_foo:string;bogus_bar:string};;

Bogus{foo="happy";bar="sad"};;
*)

let type_declaration =
    (Obj.magic (Grammar.Entry.find str_item "type_declaration") :
       MLast.type_decl Grammar.Entry.e);;
let type_parameters =
    (Obj.magic (Grammar.Entry.find type_declaration "type_parameters") :
       ((string * (bool * bool)) list) Grammar.Entry.e);;
let type_patt =
    (Obj.magic (Grammar.Entry.find type_declaration "type_patt") :
       (MLast.loc * string) Grammar.Entry.e);;
let type_kind =
    (Obj.magic (Grammar.Entry.find type_declaration "type_kind") :
        MLast.ctyp Grammar.Entry.e);;
let constrain =
    (Obj.magic (Grammar.Entry.find type_declaration "constrain") :
        ((MLast.ctyp * MLast.ctyp)) Grammar.Entry.e);;


let prepend_id_t name tk =
  let helper = fun
    (loc,s,b,t) -> (loc,name ^ "_" ^ s,b,t) in
  match tk with
    <:ctyp< { $list:ldl$ }>> ->
      let ldl2 = List.map helper ldl in <:ctyp< { $list:ldl2$ }>>
  | _ -> tk;;



EXTEND
  type_declaration:
    [ [ tpl = type_parameters; n = type_patt; "="; "LOCAL"; tk = type_kind;
        cl = LIST0 constrain ->
	  match n with (loc,s) ->
	    make_record_constructor s;
	    (n, tpl, prepend_id_t s tk,  cl) ] ]
  ;
END;;


(* example: *)
type person = LOCAL {name:string; addr: string};;
let p = Person{name="Joe";addr="1 Broadway Ave"};;
match p with
  Person{name=n;addr=a} -> (n,a);;


(* this sort of thing works too: *)
type person = LOCAL {name:string; addr:string; friends:plist}
and plist = Plist_person of (person * plist) | List_End;;



-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] record labels of record scope using camlp4
  2002-01-14  7:28 [Caml-list] record labels of record scope using camlp4 Jeff Henrikson
@ 2002-01-14 10:01 ` Daniel de Rauglaudre
  2002-01-14 12:15 ` Alain Frisch
  1 sibling, 0 replies; 6+ messages in thread
From: Daniel de Rauglaudre @ 2002-01-14 10:01 UTC (permalink / raw)
  To: caml-list

[-- Attachment #1: Type: text/plain, Size: 917 bytes --]

Hi,

On Mon, Jan 14, 2002 at 02:28:02AM -0500, Jeff Henrikson wrote:

> First bad news: the code runs great in the toplevel, but that's
> because camlp4 extends grammars interactively there.  Apparently in
> batch compilation, any EXTEND construct only takes effect at the end
> of the file, not at the end of a statement.

It works in batch mode, but syntax extensions must be given to camlp4
as cmo or cma files. Find attached your program and its two examples.
First compile your extension by:
      ocamlc -pp camlp4o -I +camlp4 -c my_extension.ml

And your two examples by:
      ocamlc -pp camlp4o -c my_file.ml
      ocamlc -pp camlp4o -c my_file2.ml

You can also pretty print the corresponding pure OCaml code, to see
what the syntax extension did, by:
      camlp4o pr_o.cmo my_file.ml
      camlp4o pr_o.cmo my_file2.ml

-- 
Daniel de RAUGLAUDRE
daniel.de_rauglaudre@inria.fr
http://cristal.inria.fr/~ddr/

[-- Attachment #2: my_extension.ml --]
[-- Type: text/plain, Size: 2423 bytes --]

#load "q_MLast.cmo";;
#load "pa_extend.cmo";;

open Pcaml;;

let loc = (0,0);;        (* toploop only *)

let prepend_id name labels =
  let helper2 v =
    match v with
    <:patt<$lid:x$>> ->
      let y = name ^ "_" ^ x in
      <:patt<$lid:y$>>
  | _ -> v in
  let helper1 = fun
    (patt,expr) -> (helper2 patt,expr) in
  List.map helper1 labels;;



let make_record_constructor name =
  let lname = String.uncapitalize name in
  let uname = String.capitalize name in
  let lbl_expr_list =
    (Obj.magic (Grammar.Entry.find expr "lbl_expr_list") :
       (MLast.patt * MLast.expr) list Grammar.Entry.e) in
  let lbl_patt_list =
    (Obj.magic (Grammar.Entry.find patt "lbl_patt_list") :
       (MLast.patt * MLast.patt) list Grammar.Entry.e) in
  EXTEND
    expr: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_expr_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:expr<{$list:memb0$}>>) ] ];
    patt: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_patt_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:patt<{$list:memb0$}>>) ] ];
  END;
;;

(* test:
make_record_constructor "bogus";;

type bogus = {bogus_foo:string;bogus_bar:string};;

Bogus{foo="happy";bar="sad"};;
*)

let type_declaration =
    (Obj.magic (Grammar.Entry.find str_item "type_declaration") :
       MLast.type_decl Grammar.Entry.e);;
let type_parameters =
    (Obj.magic (Grammar.Entry.find type_declaration "type_parameters") :
       ((string * (bool * bool)) list) Grammar.Entry.e);;
let type_patt =
    (Obj.magic (Grammar.Entry.find type_declaration "type_patt") :
       (MLast.loc * string) Grammar.Entry.e);;
let type_kind =
    (Obj.magic (Grammar.Entry.find type_declaration "type_kind") :
        MLast.ctyp Grammar.Entry.e);;
let constrain =
    (Obj.magic (Grammar.Entry.find type_declaration "constrain") :
        ((MLast.ctyp * MLast.ctyp)) Grammar.Entry.e);;


let prepend_id_t name tk =
  let helper = fun
    (loc,s,b,t) -> (loc,name ^ "_" ^ s,b,t) in
  match tk with
    <:ctyp< { $list:ldl$ }>> ->
      let ldl2 = List.map helper ldl in <:ctyp< { $list:ldl2$ }>>
  | _ -> tk;;



EXTEND
  type_declaration:
    [ [ tpl = type_parameters; n = type_patt; "="; "LOCAL"; tk = type_kind;
        cl = LIST0 constrain ->
	  match n with (loc,s) ->
	    make_record_constructor s;
	    (n, tpl, prepend_id_t s tk,  cl) ] ]
  ;
END;;

[-- Attachment #3: my_file.ml --]
[-- Type: text/plain, Size: 194 bytes --]

#load "./my_extension.cmo";;

(* example: *)
type person = LOCAL {name:string; addr: string};;
let p = Person{name="Joe";addr="1 Broadway Ave"};;
match p with
  Person{name=n;addr=a} -> (n,a);;

[-- Attachment #4: my_file2.ml --]
[-- Type: text/plain, Size: 186 bytes --]

#load "./my_extension.cmo";;

(* this sort of thing works too: *)
type person = LOCAL {name:string; addr:string; friends:plist}
and plist = Plist_person of (person * plist) | List_End;;

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] record labels of record scope using camlp4
  2002-01-14  7:28 [Caml-list] record labels of record scope using camlp4 Jeff Henrikson
  2002-01-14 10:01 ` Daniel de Rauglaudre
@ 2002-01-14 12:15 ` Alain Frisch
  2002-01-17 18:06   ` Didier Remy
  1 sibling, 1 reply; 6+ messages in thread
From: Alain Frisch @ 2002-01-14 12:15 UTC (permalink / raw)
  To: Jeff Henrikson; +Cc: caml-list

On Mon, 14 Jan 2002, Jeff Henrikson wrote:

> It seems like a lot of people dislike the fact that record labels have
> module scope instead of record scope.

I think it should be possible to minimize the incovenience without
rejecting module scope. For instance, one could introduce the syntax:

module_path.{ x1 = e1; ....; xn = en }

as an abbreviation for:

{ module_path.x1 = e1; ...; module_path.xn = en }


Another way would be to use type information provided by the type-checker.
For accessing field, here is a description of a simple patch to the
type-checker that could be handy; the idea is that, when the expression e
is known to be a record of a given type, one can use a "record scope" rule
for label. A ten-minutes hack leads me to:

in typing/types.{ml,mli}:
...
and type_kind =
    Type_abstract
  | Type_variant of (string * type_expr list) list
  | Type_record of (string * mutable_flag * type_expr * label_description
option ref) list * record_representation
...
(note the extra (dirty) label_description option ref)

and trivial modification in many places ...

in typing/datarepr.ml:
let label_descrs ty_res lbls repres =
  let all_labels = Array.create (List.length lbls) dummy_label in
  let rec describe_labels num = function
      [] -> []
    | (name, mut_flag, ty_arg, l) :: rest ->
        let lbl =
          { lbl_res = ty_res;
            lbl_arg = ty_arg;
            lbl_mut = mut_flag;
            lbl_pos = num;
            lbl_all = all_labels;
            lbl_repres = repres } in
	(match !l with
	  | Some _ -> ()  (* Note sure what this corresponds to *)
	  | None -> l := Some lbl);
        all_labels.(num) <- lbl;
        (name, lbl) :: describe_labels (num+1) rest in
  describe_labels 0 lbls


in typing/typedecl.ml:
        | Ptype_record lbls ->
            let all_labels = ref StringSet.empty in
            List.iter
              (fun (name, mut, arg) ->
                if StringSet.mem name !all_labels then
                  raise(Error(sdecl.ptype_loc, Duplicate_label name));
                all_labels := StringSet.add name !all_labels)
              lbls;
            let lbls' =
              List.map
                (fun (name, mut, arg) ->
                         (name, mut, transl_simple_type env true arg, ref
None))
                lbls in
            let rep =
              if List.for_all (fun (name, mut, arg, _) -> is_float env
arg) lbls
'
              then Record_float
              else Record_regular in
            Type_record(lbls', rep)
        end;
      type_manifest =

(note the extra ref None)

in typing/typecore.ml (function type_exp):
  | Pexp_field(sarg, lid) ->
      let arg = type_exp env sarg in
      let label =
        try
	  try
	    match lid with
	      | Longident.Lident name -> lookup_label arg.exp_type name env
	      | _ -> raise Not_found
          with Not_found -> Env.lookup_label lid env
        with Not_found ->
          raise(Error(sexp.pexp_loc, Unbound_label lid)) in
      let (ty_arg, ty_res) = instance_label label in

where lookup_label is defined as:

let rec lookup_label ty name env =
  let ty = repr ty in
  match ty.desc with
  | Tconstr (path, _, _) ->
      let td = Env.find_type path env in
      begin match td.type_kind with
      | Type_record (fields, _) ->
	  let (_,_,_,l) =
	    List.find (fun (name', _, _,_) -> name = name') fields in
	  (match !l with
	      Some l -> l
	    | None -> raise Not_found)
      | Type_abstract when td.type_manifest <> None ->
          lookup_label (expand_head env ty) name env
      | _ -> raise Not_found
      end
  | _ ->
      raise Not_found



This patch allows to write things like:

type t = { x : int; y : int };;
type s = { x : string };;
let f (t : t) : int = t.x;;

The same could be done for record expression (in function type_expect):

let r : t = { x = 2; y = 3 } in
...


On the one hand, this kind of interaction with the type-checker is quite
dangerous as it breaks complete type inference, but in OCaml, there are
already some cases where type annotations are mandatory; on the other
hand, it leads to a lightweight syntax and is probably what programmers
expect.


(sorry for suggesting a solution to a non-issue ;) )

-- 
  Alain

-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] record labels of record scope using camlp4
  2002-01-14 12:15 ` Alain Frisch
@ 2002-01-17 18:06   ` Didier Remy
  2002-01-17 19:58     ` Alain Frisch
  0 siblings, 1 reply; 6+ messages in thread
From: Didier Remy @ 2002-01-17 18:06 UTC (permalink / raw)
  To: Alain Frisch; +Cc: Jeff Henrikson, caml-list

> Another way would be to use type information provided by the type-checker.
> For accessing field, here is a description of a simple patch to the
> type-checker that could be handy; the idea is that, when the expression e
> is known to be a record of a given type, one can use a "record scope" rule
> for label. 

Isn't this just a form of static and local resolution of overloading?
However, local resolution does not commute with unification...
Hence, the specification of well-typed programs should then strongly
depend on the order in which unifications (i.e. type inference) are 
performed. Do you have a specification of well-typed programs but
the type-inference algorithm itself? 

> A ten-minutes hack leads me to:

[...]

> This patch allows to write things like:
> 
> type t = { x : int; y : int };;
> type s = { x : string };;
> let f (t : t) : int = t.x;;
> 
> The same could be done for record expression (in function type_expect):
> 
> let r : t = { x = 2; y = 3 } in
> ...

> On the one hand, this kind of interaction with the type-checker is quite
> dangerous as it breaks complete type inference, but in OCaml, there are
> already some cases where type annotations are mandatory; on the other
> hand, it leads to a lightweight syntax and is probably what programmers
> expect.

It is true that Ocaml differs from the nice theory of core ML in a few
places.  However, we have tried to keep those differences as unsignificant
as possible, and as few as possible.

For example, a module with a weak type variable in its principal signature
is rejected, while any ground instantiation of this weak type variable would
be accepted, so, yes: ``Ocaml does not have principal types''. However, type
inference is still easy to specify, and in particular does not rely in which
operations are performed.

        Didier
-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] record labels of record scope using camlp4
  2002-01-17 18:06   ` Didier Remy
@ 2002-01-17 19:58     ` Alain Frisch
  2002-01-18  6:58       ` Jacques Garrigue
  0 siblings, 1 reply; 6+ messages in thread
From: Alain Frisch @ 2002-01-17 19:58 UTC (permalink / raw)
  To: Didier Remy; +Cc: Caml list

On Thu, 17 Jan 2002, Didier Remy wrote:

> > Another way would be to use type information provided by the type-checker.
> > For accessing field, here is a description of a simple patch to the
> > type-checker that could be handy; the idea is that, when the expression e
> > is known to be a record of a given type, one can use a "record scope" rule
> > for label.
>
> Isn't this just a form of static and local resolution of overloading?

Yes.

> However, local resolution does not commute with unification...
> Hence, the specification of well-typed programs should then strongly
> depend on the order in which unifications (i.e. type inference) are
> performed. Do you have a specification of well-typed programs but
> the type-inference algorithm itself?

No, but I'm not sure how important it is for the ``Real Life''.
How many people fully understand OCaml type system (including subtleties
with variant types and objects) ?  Maybe it is because I am just a
newcomer to OCaml (3 years), but I don't, and the fact that the full type
system _could_ be formalized in a nice formal system is not a big help
for me _as a programmer_ (and as far as I can tell, OCaml type system
is fully specified nowhere).

For the specific case, it is easy to give informal sufficient conditions
of well-typedness ("a type annotation must allow to know the record
type constructor [not the type arguments] for the record expression
before its use"). Note that this condition is too strong, as
it does not cover this case:

# type t = { x : int };;
type t = { x : int; }
# let f () = { x = 2 };;
val f : unit -> t = <fun>
# type s = { x : string };;
type s = { x : string; }
# (f ()).x;;
- : int = 2

> It is true that Ocaml differs from the nice theory of core ML in a few
> places.  However, we have tried to keep those differences as
> unsignificant as possible, and as few as possible.
>
> For example, a module with a weak type variable in its principal signature
> is rejected, while any ground instantiation of this weak type variable would
> be accepted, so, yes: ``Ocaml does not have principal types''. However, type
> inference is still easy to specify, and in particular does not rely in which
> operations are performed.

Note that the patch I proposed was not a suggestion for inclusion in
OCaml; I think it solves in a light way a practical problem (theoretical
non-issue) encountered by many people, but I know it is not really in
OCaml spirit.




Alain

-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [Caml-list] record labels of record scope using camlp4
  2002-01-17 19:58     ` Alain Frisch
@ 2002-01-18  6:58       ` Jacques Garrigue
  0 siblings, 0 replies; 6+ messages in thread
From: Jacques Garrigue @ 2002-01-18  6:58 UTC (permalink / raw)
  To: frisch; +Cc: caml-list

From: Alain Frisch <frisch@clipper.ens.fr>

> Note that the patch I proposed was not a suggestion for inclusion in
> OCaml; I think it solves in a light way a practical problem (theoretical
> non-issue) encountered by many people, but I know it is not really in
> OCaml spirit.

I actually suggested such a patch about a year ago, and then sawed my
own branch by giving the following counter-example:

        type t = {a : u}
        and u = {a : t}

        let f x y =
          (x.a == y) && (x == y.a)

If you type first (x.a == y), you get x : u, and as a result y : t,
but if you start with (x == y.a)  you obtain y : u and x : t.

That's bad, but not really dangerous, I must say. And that's about as
bad as you can get.

Maybe we will be able to do that once polymorphic methods are
included... (Polymorphic methods are not relevant here, but they might
be accompanied by a mechanism to rule out the above example)

Cheers,

Jacques Garrigue
-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2002-01-18  7:37 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2002-01-14  7:28 [Caml-list] record labels of record scope using camlp4 Jeff Henrikson
2002-01-14 10:01 ` Daniel de Rauglaudre
2002-01-14 12:15 ` Alain Frisch
2002-01-17 18:06   ` Didier Remy
2002-01-17 19:58     ` Alain Frisch
2002-01-18  6:58       ` Jacques Garrigue

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).