From 2307ccdcc5eb4ddfe719ddcbea999f7705ec79c3 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Thu, 30 Apr 2009 17:15:14 -0400 Subject: C FFI compiler options --- .hgignore | 6 ++- include/types.h | 4 ++ include/urweb.h | 5 +++ src/cjr_print.sig | 2 - src/cjr_print.sml | 12 +++--- src/compiler.sig | 7 +++- src/compiler.sml | 120 +++++++++++++++++++++++++++++++++++++----------------- src/corify.sml | 2 +- src/demo.sml | 5 ++- src/jscomp.sml | 2 +- src/monoize.sig | 2 - src/monoize.sml | 18 +++----- src/settings.sig | 39 ++++++++++++++++++ src/settings.sml | 49 ++++++++++++++++++++++ src/sources | 3 ++ tests/Makefile | 4 ++ tests/cffi.ur | 3 ++ tests/cffi.urp | 6 +++ tests/cffi.urs | 1 + tests/test.c | 15 +++++++ tests/test.h | 7 ++++ tests/test.urs | 5 +++ 22 files changed, 251 insertions(+), 66 deletions(-) create mode 100644 src/settings.sig create mode 100644 src/settings.sml create mode 100644 tests/Makefile create mode 100644 tests/cffi.ur create mode 100644 tests/cffi.urp create mode 100644 tests/cffi.urs create mode 100644 tests/test.c create mode 100644 tests/test.h create mode 100644 tests/test.urs diff --git a/.hgignore b/.hgignore index c0222189..f51f3c32 100644 --- a/.hgignore +++ b/.hgignore @@ -13,7 +13,6 @@ src/urweb.mlb *.grm.* *.o -./Makefile src/config.sml *.exe @@ -37,3 +36,8 @@ demo/demo.* .depend Makefile.coq *.vo + +syntax: regexp + +^Makefile +^src/coq/Makefile diff --git a/include/types.h b/include/types.h index 1227a4db..0dbd6118 100644 --- a/include/types.h +++ b/include/types.h @@ -1,3 +1,6 @@ +#ifndef URWEB_TYPES_H +#define URWEB_TYPES_H + #include typedef long long uw_Basis_int; @@ -42,3 +45,4 @@ typedef struct input *uw_input; #define FLOATS_MAX 100 #define TIMES_MAX 100 +#endif diff --git a/include/urweb.h b/include/urweb.h index c57cc317..27ab799f 100644 --- a/include/urweb.h +++ b/include/urweb.h @@ -1,3 +1,6 @@ +#ifndef URWEB_H +#define URWEB_H + #include #include "types.h" @@ -176,3 +179,5 @@ uw_Basis_blob uw_Basis_fileData(uw_context, uw_Basis_file); uw_Basis_int uw_Basis_blobSize(uw_context, uw_Basis_blob); __attribute__((noreturn)) void uw_return_blob(uw_context, uw_Basis_blob, uw_Basis_string mimeType); + +#endif diff --git a/src/cjr_print.sig b/src/cjr_print.sig index d7fb21a0..baef005e 100644 --- a/src/cjr_print.sig +++ b/src/cjr_print.sig @@ -36,6 +36,4 @@ signature CJR_PRINT = sig val p_sql : CjrEnv.env -> Cjr.file Print.printer val debug : bool ref - - val timeout : int ref end diff --git a/src/cjr_print.sml b/src/cjr_print.sml index 95ae53b8..e1d6d88b 100644 --- a/src/cjr_print.sml +++ b/src/cjr_print.sml @@ -1250,8 +1250,6 @@ fun urlify env t = urlify' IS.empty 0 t end -val timeout = ref 0 - fun p_exp' par env (e, loc) = case e of EPrim p => Prim.p_t_GCC p @@ -2832,7 +2830,7 @@ fun p_file env (ds, ps) = string (case side of ServerOnly => "" | _ => "\\n"), string "\");", @@ -2844,7 +2842,7 @@ fun p_file env (ds, ps) = string ");", newline, string "uw_set_url_prefix(ctx, \"", - string (!Monoize.urlPrefix), + string (Settings.getUrlPrefix ()), string "\");", newline]), string "uw_set_needs_sig(ctx, ", @@ -3185,6 +3183,10 @@ fun p_file env (ds, ps) = else box [], newline, + p_list_sep (box []) (fn s => box [string "#include \"", + string s, + string "\"", + newline]) (Settings.getHeaders ()), string "#include \"", string (OS.Path.joinDirFile {dir = Config.includ, file = "urweb.h"}), @@ -3198,7 +3200,7 @@ fun p_file env (ds, ps) = string ";", newline, string "int uw_timeout = ", - string (Int.toString (!timeout)), + string (Int.toString (Settings.getTimeout ())), string ";", newline, newline, diff --git a/src/compiler.sig b/src/compiler.sig index d00f111e..cae86472 100644 --- a/src/compiler.sig +++ b/src/compiler.sig @@ -37,11 +37,14 @@ signature COMPILER = sig sql : string option, debug : bool, profile : bool, - timeout : int + timeout : int, + ffi : string list, + link : string list, + headers : string list } val compile : string -> unit val compileC : {cname : string, oname : string, ename : string, libs : string, - profile : bool, debug : bool} -> unit + profile : bool, debug : bool, link : string list} -> unit type ('src, 'dst) phase type ('src, 'dst) transform diff --git a/src/compiler.sml b/src/compiler.sml index 99954958..a5360f89 100644 --- a/src/compiler.sml +++ b/src/compiler.sml @@ -1,4 +1,4 @@ -(* Copyright (c) 2008, Adam Chlipala +(* Copyright (c) 2008-2009, Adam Chlipala * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,8 +25,6 @@ * POSSIBILITY OF SUCH DAMAGE. *) -(* Ur/Web language parser *) - structure Compiler :> COMPILER = struct structure UrwebLrVals = UrwebLrValsFn(structure Token = LrParser.Token) @@ -43,7 +41,10 @@ type job = { sql : string option, debug : bool, profile : bool, - timeout : int + timeout : int, + ffi : string list, + link : string list, + headers : string list } type ('src, 'dst) phase = { @@ -201,7 +202,7 @@ val parseUr = { handle LrParser.ParseError => [], print = SourcePrint.p_file} -fun p_job {prefix, database, exe, sql, sources, debug, profile, timeout} = +fun p_job {prefix, database, exe, sql, sources, debug, profile, timeout, ffi, link, headers} = let open Print.PD open Print @@ -228,6 +229,9 @@ fun p_job {prefix, database, exe, sql, sources, debug, profile, timeout} = string "Timeout: ", string (Int.toString timeout), newline, + p_list_sep (box []) (fn s => box [string "Ffi", space, string s, newline]) ffi, + p_list_sep (box []) (fn s => box [string "Header", space, string s, newline]) headers, + p_list_sep (box []) (fn s => box [string "Link", space, string s, newline]) link, string "Sources:", p_list string sources, newline] @@ -251,6 +255,10 @@ val parseUrp = { OS.Path.concat (dir, fname) handle OS.Path.Path => fname + val absDir = OS.Path.mkAbsolute {path = dir, relativeTo = OS.FileSys.getDir ()} + + fun relifyA fname = OS.Path.mkAbsolute {path = fname, relativeTo = absDir} + fun readSources acc = case TextIO.inputLine inf of NONE => rev acc @@ -270,21 +278,35 @@ val parseUrp = { readSources acc end - fun finish (prefix, database, exe, sql, debug, profile, timeout, sources) = - {prefix = Option.getOpt (prefix, "/"), - database = database, - exe = Option.getOpt (exe, OS.Path.joinBaseExt {base = OS.Path.base filename, - ext = SOME "exe"}), - sql = sql, - debug = debug, - profile = profile, - timeout = Option.getOpt (timeout, 60), + val prefix = ref NONE + val database = ref NONE + val exe = ref NONE + val sql = ref NONE + val debug = ref false + val profile = ref false + val timeout = ref NONE + val ffi = ref [] + val link = ref [] + val headers = ref [] + + fun finish sources = + {prefix = Option.getOpt (!prefix, "/"), + database = !database, + exe = Option.getOpt (!exe, OS.Path.joinBaseExt {base = OS.Path.base filename, + ext = SOME "exe"}), + sql = !sql, + debug = !debug, + profile = !profile, + timeout = Option.getOpt (!timeout, 60), + ffi = !ffi, + link = !link, + headers = !headers, sources = sources} - fun read (prefix, database, exe, sql, debug, profile, timeout) = + fun read () = case TextIO.inputLine inf of - NONE => finish (prefix, database, exe, sql, debug, profile, timeout, []) - | SOME "\n" => finish (prefix, database, exe, sql, debug, profile, timeout, readSources []) + NONE => finish [] + | SOME "\n" => finish (readSources []) | SOME line => let val (cmd, arg) = Substring.splitl (fn x => not (Char.isSpace x)) (Substring.full line) @@ -293,41 +315,45 @@ val parseUrp = { in case cmd of "prefix" => - (case prefix of + (case !prefix of NONE => () | SOME _ => ErrorMsg.error "Duplicate 'prefix' directive"; - read (SOME arg, database, exe, sql, debug, profile, timeout)) + prefix := SOME arg) | "database" => - (case database of + (case !database of NONE => () | SOME _ => ErrorMsg.error "Duplicate 'database' directive"; - read (prefix, SOME arg, exe, sql, debug, profile, timeout)) + database := SOME arg) | "exe" => - (case exe of + (case !exe of NONE => () | SOME _ => ErrorMsg.error "Duplicate 'exe' directive"; - read (prefix, database, SOME (relify arg), sql, debug, profile, timeout)) + exe := SOME (relify arg)) | "sql" => - (case sql of + (case !sql of NONE => () | SOME _ => ErrorMsg.error "Duplicate 'sql' directive"; - read (prefix, database, exe, SOME (relify arg), debug, profile, timeout)) - | "debug" => read (prefix, database, exe, sql, true, profile, timeout) - | "profile" => read (prefix, database, exe, sql, debug, true, timeout) + sql := SOME (relify arg)) + | "debug" => debug := true + | "profile" => profile := true | "timeout" => - (case timeout of + (case !timeout of NONE => () | SOME _ => ErrorMsg.error "Duplicate 'timeout' directive"; - read (prefix, database, exe, sql, debug, profile, SOME (valOf (Int.fromString arg)))) - | _ => (ErrorMsg.error ("Unrecognized command '" ^ cmd ^ "'"); - read (prefix, database, exe, sql, debug, profile, timeout)) + timeout := SOME (valOf (Int.fromString arg))) + | "ffi" => ffi := relify arg :: !ffi + | "link" => link := relifyA arg :: !link + | "include" => headers := relifyA arg :: !headers + | _ => ErrorMsg.error ("Unrecognized command '" ^ cmd ^ "'"); + read () end - val job = read (NONE, NONE, NONE, NONE, false, false, NONE) + val job = read () in TextIO.closeIn inf; - Monoize.urlPrefix := #prefix job; - CjrPrint.timeout := #timeout job; + Settings.setUrlPrefix (#prefix job); + Settings.setTimeout (#timeout job); + Settings.setHeaders (#headers job); job end, print = p_job @@ -339,10 +365,24 @@ fun capitalize "" = "" | capitalize s = str (Char.toUpper (String.sub (s, 0))) ^ String.extract (s, 1, NONE) val parse = { - func = fn {database, sources = fnames, ...} : job => + func = fn {database, sources = fnames, ffi, ...} : job => let fun nameOf fname = capitalize (OS.Path.file fname) + fun parseFfi fname = + let + val mname = nameOf fname + val urs = OS.Path.joinBaseExt {base = fname, ext = SOME "urs"} + + val loc = {file = urs, + first = ErrorMsg.dummyPos, + last = ErrorMsg.dummyPos} + + val sgn = (Source.SgnConst (#func parseUrs urs), loc) + in + (Source.DFfiStr (mname, sgn), loc) + end + fun parseOne fname = let val mname = nameOf fname @@ -367,12 +407,14 @@ val parse = { (Source.DStr (mname, sgnO, (Source.StrConst ds, loc)), loc) end + val dsFfi = map parseFfi ffi val ds = map parseOne fnames in let val final = nameOf (List.last fnames) - val ds = ds @ [(Source.DExport (Source.StrVar final, ErrorMsg.dummySpan), ErrorMsg.dummySpan)] + val ds = dsFfi @ ds + @ [(Source.DExport (Source.StrVar final, ErrorMsg.dummySpan), ErrorMsg.dummySpan)] in case database of NONE => ds @@ -605,7 +647,7 @@ val sqlify = { val toSqlify = transform sqlify "sqlify" o toMono_opt2 -fun compileC {cname, oname, ename, libs, profile, debug} = +fun compileC {cname, oname, ename, libs, profile, debug, link = link'} = let val urweb_o = clibFile "urweb.o" val driver_o = clibFile "driver.o" @@ -624,6 +666,8 @@ fun compileC {cname, oname, ename, libs, profile, debug} = (compile ^ " -g", link ^ " -g") else (compile, link) + + val link = foldl (fn (s, link) => link ^ " " ^ s) link link' in if not (OS.Process.isSuccess (OS.Process.system compile)) then print "C compilation failed\n" @@ -689,7 +733,7 @@ fun compile job = end; compileC {cname = cname, oname = oname, ename = ename, libs = libs, - profile = #profile job, debug = #debug job}; + profile = #profile job, debug = #debug job, link = #link job}; cleanup () end diff --git a/src/corify.sml b/src/corify.sml index f1895e19..19568b8b 100644 --- a/src/corify.sml +++ b/src/corify.sml @@ -890,7 +890,7 @@ fun corifyDecl mods (all as (d, loc : EM.span), st) = val st = St.bindStr st m n (St.ffi m cmap conmap) in - (rev ds, St.basisIs (st, n)) + (rev ds, if m = "Basis" then St.basisIs (st, n) else st) end | _ => raise Fail "Non-const signature for FFI structure") diff --git a/src/demo.sml b/src/demo.sml index 43fa5ef0..4e73faea 100644 --- a/src/demo.sml +++ b/src/demo.sml @@ -94,7 +94,10 @@ fun make {prefix, dirname, guided} = file = "demo.sql"}), debug = false, timeout = Int.max (#timeout combined, #timeout urp), - profile = false + profile = false, + ffi = [], + link = [], + headers = [] } val parse = Compiler.run (Compiler.transform Compiler.parseUrp "Demo parseUrp") diff --git a/src/jscomp.sml b/src/jscomp.sml index 0f545987..1c5132c2 100644 --- a/src/jscomp.sml +++ b/src/jscomp.sml @@ -965,7 +965,7 @@ fun process file = val (ek, st) = jsE inner (ek, st) val (unurl, st) = unurlifyExp loc (t, st) in - (strcat [str ("rc(cat(\"" ^ !Monoize.urlPrefix ^ "\","), + (strcat [str ("rc(cat(\"" ^ Settings.getUrlPrefix () ^ "\","), e, str ("), function(s){var t=s.split(\"/\");var i=0;return " ^ unurl ^ "},"), diff --git a/src/monoize.sig b/src/monoize.sig index 4e02e5ea..838d7c4c 100644 --- a/src/monoize.sig +++ b/src/monoize.sig @@ -27,8 +27,6 @@ signature MONOIZE = sig - val urlPrefix : string ref - val monoize : CoreEnv.env -> Core.file -> Mono.file val liftExpInExp : int -> Mono.exp -> Mono.exp diff --git a/src/monoize.sml b/src/monoize.sml index c54ab8ba..877e1a2c 100644 --- a/src/monoize.sml +++ b/src/monoize.sml @@ -36,8 +36,6 @@ structure L' = Mono structure IM = IntBinaryMap structure IS = IntBinarySet -val urlPrefix = ref "/" - val dummyTyp = (L'.TDatatype (0, ref (L'.Enum, [])), E.dummySpan) structure U = MonoUtil @@ -376,7 +374,7 @@ fun fooifyExp fk env = let val (_, _, _, s) = Env.lookupENamed env fnam in - ((L'.EPrim (Prim.String (!urlPrefix ^ s)), loc), fm) + ((L'.EPrim (Prim.String (Settings.getUrlPrefix () ^ s)), loc), fm) end | L'.EClosure (fnam, args) => let @@ -399,7 +397,7 @@ fun fooifyExp fk env = | _ => (E.errorAt loc "Type mismatch encoding attribute"; (e, fm)) in - attrify (args, ft, (L'.EPrim (Prim.String (!urlPrefix ^ s)), loc), fm) + attrify (args, ft, (L'.EPrim (Prim.String (Settings.getUrlPrefix () ^ s)), loc), fm) end | _ => case t of @@ -1257,7 +1255,8 @@ fun monoExp (env, st, fm) (all as (e, loc)) = ((L'.EAbs ("c", s, (L'.TFun (t, (L'.TFun (un, un), loc)), loc), (L'.EAbs ("v", t, (L'.TFun (un, un), loc), (L'.EAbs ("_", un, un, - (L'.EFfiApp ("Basis", "set_cookie", [(L'.EPrim (Prim.String (!urlPrefix)), + (L'.EFfiApp ("Basis", "set_cookie", [(L'.EPrim (Prim.String + (Settings.getUrlPrefix ())), loc), (L'.ERel 2, loc), e]), loc)), @@ -3138,14 +3137,7 @@ datatype expungable = Client | Channel fun monoize env file = let - val p = !urlPrefix - val () = - if p = "" then - urlPrefix := "/" - else if String.sub (p, size p - 1) <> #"/" then - urlPrefix := p ^ "/" - else - () + (* Calculate which exported functions need cookie signature protection *) val rcook = foldl (fn ((d, _), rcook) => diff --git a/src/settings.sig b/src/settings.sig new file mode 100644 index 00000000..ba4e1b9a --- /dev/null +++ b/src/settings.sig @@ -0,0 +1,39 @@ +(* Copyright (c) 2008-2009, Adam Chlipala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - The names of contributors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *) + +signature SETTINGS = sig + + val setUrlPrefix : string -> unit + val getUrlPrefix : unit -> string + + val setTimeout : int -> unit + val getTimeout : unit -> int + + val setHeaders : string list -> unit + val getHeaders : unit -> string list + +end diff --git a/src/settings.sml b/src/settings.sml new file mode 100644 index 00000000..1bc14776 --- /dev/null +++ b/src/settings.sml @@ -0,0 +1,49 @@ +(* Copyright (c) 2008-2009, Adam Chlipala + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - The names of contributors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *) + +structure Settings :> SETTINGS = struct + +val urlPrefix = ref "/" +val timeout = ref 0 +val headers = ref ([] : string list) + +fun getUrlPrefix () = !urlPrefix +fun setUrlPrefix p = + urlPrefix := (if p = "" then + "/" + else if String.sub (p, size p - 1) <> #"/" then + p ^ "/" + else + p) + +fun getTimeout () = !timeout +fun setTimeout n = timeout := n + +fun getHeaders () = !headers +fun setHeaders ls = headers := ls + +end diff --git a/src/sources b/src/sources index 43ee21b3..0f39fc74 100644 --- a/src/sources +++ b/src/sources @@ -13,6 +13,9 @@ order.sml errormsg.sig errormsg.sml +settings.sig +settings.sml + print.sig print.sml diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 00000000..5313d12d --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,4 @@ +all: test.o + +test.o: test.c + gcc -c test.c -o test.o diff --git a/tests/cffi.ur b/tests/cffi.ur new file mode 100644 index 00000000..2e20199b --- /dev/null +++ b/tests/cffi.ur @@ -0,0 +1,3 @@ +fun main () = return + {[Test.out (Test.frob (Test.create "Hello ") "world!")]} + diff --git a/tests/cffi.urp b/tests/cffi.urp new file mode 100644 index 00000000..cb27c744 --- /dev/null +++ b/tests/cffi.urp @@ -0,0 +1,6 @@ +debug +ffi test +include test.h +link test.o + +cffi diff --git a/tests/cffi.urs b/tests/cffi.urs new file mode 100644 index 00000000..6ac44e0b --- /dev/null +++ b/tests/cffi.urs @@ -0,0 +1 @@ +val main : unit -> transaction page diff --git a/tests/test.c b/tests/test.c new file mode 100644 index 00000000..c40eb966 --- /dev/null +++ b/tests/test.c @@ -0,0 +1,15 @@ +#include "../include/urweb.h" + +typedef uw_Basis_string uw_Test_t; + +uw_Test_t uw_Test_create(uw_context ctx, uw_Basis_string s) { + return s; +} + +uw_Basis_string uw_Test_out(uw_context ctx, uw_Test_t s) { + return s; +} + +uw_Test_t uw_Test_frob(uw_context ctx, uw_Test_t s1, uw_Basis_string s2) { + return uw_Basis_strcat(ctx, s1, s2); +} diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 00000000..4cd257f8 --- /dev/null +++ b/tests/test.h @@ -0,0 +1,7 @@ +#include "../include/urweb.h" + +typedef uw_Basis_string uw_Test_t; + +uw_Test_t uw_Test_create(uw_context, uw_Basis_string); +uw_Basis_string uw_Test_out(uw_context, uw_Test_t); +uw_Test_t uw_Test_frob(uw_context, uw_Test_t, uw_Basis_string); diff --git a/tests/test.urs b/tests/test.urs new file mode 100644 index 00000000..e354572c --- /dev/null +++ b/tests/test.urs @@ -0,0 +1,5 @@ +type t + +val create : string -> t +val out : t -> string +val frob : t -> string -> t -- cgit v1.2.3