From 850563d90f0f66b9563e0e0c9a24eac6bf78df7e Mon Sep 17 00:00:00 2001 From: TheNotary Date: Fri, 21 Oct 2016 14:06:41 -0500 Subject: reflows intro tutorial --- demo/prose | 73 +++++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/demo/prose b/demo/prose index 8637ef68..7c45a267 100644 --- a/demo/prose +++ b/demo/prose @@ -1,38 +1,81 @@

Ur/Web is a domain-specific language for programming web applications backed by SQL databases. It is (strongly) statically-typed (like ML and Haskell) and purely functional (like Haskell). Ur is the base language, and the web-specific features of Ur/Web (mostly) come only in the form of special rules for parsing and optimization. The Ur core looks a lot like Standard ML, with a few Haskell-isms added, and kinder, gentler versions added of many features from dependently-typed languages like the logic behind Coq. The type system is much more expressive than in ML and Haskell, such that well-typed web applications cannot "go wrong," not just in handling single HTTP requests, but across their entire lifetimes of interacting with HTTP clients. Beyond that, Ur is unusual in using ideas from dependent typing to enable very effective metaprogramming, or programming with explicit analysis of type structure. Many common web application components can be built by Ur/Web functions that operate on types, where it seems impossible to achieve similar code re-use in more established statically-typed languages.

-

This demo is built automatically from Ur/Web sources and supporting files. If you unpack the Ur/Web source distribution, then the following steps will (if you're lucky) build you a local version of this demo. If you're not lucky, you can consult the beginning of the manual for more detailed instructions. +

The page you are currently reading is a part of the demo included with the Ur/Web sources and supporting files available from github. The following steps will build a local instance of the demo if you're lucky (and running a debian based linux OS). If you're not lucky, you can consult the beginning of the manual for more detailed instructions.

-
./configure
+
Install System Dependencies
+

+

sudo apt-get install build-essential \
+  emacs-goodies-el \
+  libgmp-dev \
+  libssl-dev \
+  libpq-dev \
+  libsqlite3-dev \
+  mlton \
+  sqlite3

+ +
Build and install the Ur/web Framework
+

./configure
 make
 sudo make install
-urweb -noEmacs -demo /Demo demo

+

-

The -demo /Demo flag says that we want to build a demo application that expects its URIs to begin with /Demo. The final argument demo gives the path to a directory housing demo files. One of the files in that directory is prose, a file describing the different demo pieces with HTML. Some lines of prose have the form foo.urp, naming particular project files (with the extension .urp) in that directory.

+
Compile the demo the easy way
+

$ urweb -dbms sqlite -db /path_to_db.sqlite -demo /Demo demo
+

-

These project files can also be built separately. For example, you could run +

The -dbms sqlite flag indicates that instead of using the default database management system of postgres, we wish to use sqlite (not good for production). The -db flag allows us to specify the path to our sqlite database on the file system. The -demo /Demo parameter indicates that we want to build a demo application that expects its URIs to begin with /Demo. The final argument demo gives the path to a directory housing Ur/Web source files (.ur, .urp, .urs, etc, etc). +

-
urweb demo/hello
+

+The following files are created during the compile process: +

+

+ +
Migrate the Database
+

+When we compiled the demo in the above step, a demo.sql file was created for us which contains all the information required to create a database congruent with the demo web app. The below command will provision our sqlite database. Also of interest is the file demo.urp which contains a database line with the PostgreSQL database that the demo web server will try to connect to if database information isn't provided as command line arguments when the webapp is compiled. -to build the "Hello World" demo application. Whether building the pieces separately or all at once with the -demo flag, a standalone web server executable is generated. The -demo command line will generate demo/demo.exe, and the other command line will generate demo/hello.exe. Either can be run with no arguments to start a single-threaded server accepting requests on port 8080. Pass the flag -h to see which options are available.

+
$ sqlite3 /path/to/database/file <demo/demo.sql
+
+

-

The -demo version also generates some HTML in a subdirectory out of the demo directory. It is easy to set Apache up to serve these HTML files, and to proxy out to the Ur/Web web server for dynamic page requests. This configuration works for me, where DIR is the location of an Ur/Web source distribution. +

Boot the web app
+Executing the binary generated above (demo/demo.exe) with no arguments will start a single-threaded server listening on port 8080. Pass the flag -h to see which options are available on such freshly built binaries. +

+

$ demo/demo.exe
+Database connection initialized.
+Listening on port 8080....
+
+Test out http://localhost:8080/Demo/Demo/main which should consist of links to the individual demos after booting the web app.

+

+ +
Serve the Static Content with a Reverse Proxy
+

The -demo version also generates some HTML in a subdirectory out of the demo directory (eg index.html). It is easy to set Apache up to serve these HTML files, and to proxy out to the Ur/Web web server for dynamic page requests. This configuration works for me, where DIR is the location of an Ur/Web source distribution.

Alias /demo/ "DIR/demo/out/"
 
 ProxyPass /Demo/ http://localhost:8080/Demo/
 ProxyPassReverse /Demo/ http://localhost:8080/Demo/

-

Building the demo also generates a demo.sql file, giving the SQL commands to run to define all of the tables and sequences that the applications expect to see. The file demo.urp contains a database line with the PostgreSQL database that the demo web server will try to connect to.

-

The easiest way to get a demo running locally is probably with this alternate command sequence: +

Compile Individually
+

These project files can also be built separately. For example, you could run + +

$ urweb demo/hello
+
-
urweb -dbms sqlite -db /path/to/database/file -demo /Demo demo
-sqlite3 /path/to/database/file <demo/demo.sql
-demo/demo.exe

+to build the "Hello World" demo application. Doing so will invite urweb to seak out the various demo/hello.* files and, from them, build a binary demo/demo.exe. The URL to access the resulting webapp will be http://localhost:8080/Hello/main. +

-

Then you can skip the static content and connect directly to the demo server at http://localhost:8080/Demo/Demo/main, which contains links to the individual demos. If you're running the server created just for hello, then the URL will be http://localhost:8080/Hello/main.

+
This File
+

One of the files in the demo directory is named prose, a file describing the different demo pieces with HTML. Some lines of prose have the form foo.urp, naming particular project files (with the extension .urp) in that directory. These makeup the different pages of the tutorial.

-

The rest of the demo focuses on the individual applications. Follow the links in the lefthand frame to visit the applications, commentary, and syntax-highlighted source code. (An Emacs mode is behind the syntax highlighting.) I recommend visiting the applications in the order listed, since that is the order in which new concepts are introduced.

+
Proceeding Demo Exhibits
+

The rest of the demo focuses on the individual facets of the Ur/ Web framework. Follow the links in the lefthand frame to visit the applications, commentary, and syntax-highlighted source code. (An Emacs mode is behind the syntax highlighting.) I recommend visiting the applications in the order listed, since that is the order in which new concepts are introduced.

hello.urp -- cgit v1.2.3 From a7f331db1fb5d9138c8909fb843989c3e4d45b42 Mon Sep 17 00:00:00 2001 From: TheNotary Date: Fri, 21 Oct 2016 14:42:59 -0500 Subject: notes a place to see where a database table is defined --- demo/prose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/prose b/demo/prose index 7c45a267..e3d6185a 100644 --- a/demo/prose +++ b/demo/prose @@ -37,7 +37,7 @@ The following files are created during the compile process:
Migrate the Database

-When we compiled the demo in the above step, a demo.sql file was created for us which contains all the information required to create a database congruent with the demo web app. The below command will provision our sqlite database. Also of interest is the file demo.urp which contains a database line with the PostgreSQL database that the demo web server will try to connect to if database information isn't provided as command line arguments when the webapp is compiled. +When we compiled the demo in the above step, a demo.sql file was created for us which contains all the information required to create a database congruent with the demo web app. The below command will provision our sqlite database. To see an example of where a database table is defined in source code, check out demo/crud1.ur. Also of interest is the file demo.urp which contains a database line with the PostgreSQL database that the demo web server will try to connect to if database information isn't provided as command line arguments when the webapp is compiled.

$ sqlite3 /path/to/database/file <demo/demo.sql
 
-- cgit v1.2.3 From d5ae848de34109b66d2ec45fd6c0ac5a06149a68 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 22 Oct 2016 09:59:00 -0400 Subject: Return to working version mode --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index fdf010b4..8ee3e795 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_INIT([urweb], [20161022]) -WORKING_VERSION=0 +WORKING_VERSION=1 AC_USE_SYSTEM_EXTENSIONS # automake 1.12 requires this, but automake 1.11 doesn't recognize it -- cgit v1.2.3 From 4d37351eab0ea02ca065f4415aa6ac556a8c7820 Mon Sep 17 00:00:00 2001 From: Benjamin Barenblat Date: Mon, 7 Nov 2016 17:31:19 -0500 Subject: FastCGI: Track request IDs during processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When processing a FastCGI request, save its request ID in thread-local storage and respond with the same request ID (rather than hard-coding 1). This fixes FastCGI on picky web servers (nginx). For more background, see https://github.com/urweb/urweb/issues/57 and ยง3.3 of the FastCGI specification. Tested with nginx 1.10.2 and lighttpd 1.4.39. Closes https://github.com/urweb/urweb/issues/57. --- configure.ac | 1 + m4/ax_tls.m4 | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/c/fastcgi.c | 45 ++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 m4/ax_tls.m4 diff --git a/configure.ac b/configure.ac index 8ee3e795..0dda6d3b 100644 --- a/configure.ac +++ b/configure.ac @@ -12,6 +12,7 @@ AC_PROG_LIBTOOL() AC_CONFIG_HEADERS([include/urweb/config.h]) AX_PTHREAD([echo >/dev/null], [echo "Your C compiler does not support POSIX threads."; exit 1]) +AX_TLS([echo >/dev/null], [echo "Your C compiler does not support thread-local storage."; exit 1]) AX_CHECK_OPENSSL([echo >/dev/null], [echo "You must install OpenSSL development files."; exit 1]) diff --git a/m4/ax_tls.m4 b/m4/ax_tls.m4 new file mode 100644 index 00000000..809b761a --- /dev/null +++ b/m4/ax_tls.m4 @@ -0,0 +1,74 @@ +# =========================================================================== +# http://www.gnu.org/software/autoconf-archive/ax_tls.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_TLS([action-if-found], [action-if-not-found]) +# +# DESCRIPTION +# +# Provides a test for the compiler support of thread local storage (TLS) +# extensions. Defines TLS if it is found. Currently knows about GCC/ICC +# and MSVC. I think SunPro uses the same as GCC, and Borland apparently +# supports either. +# +# LICENSE +# +# Copyright (c) 2008 Alan Woodland +# Copyright (c) 2010 Diego Elio Petteno` +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 11 + +AC_DEFUN([AX_TLS], [ + AC_MSG_CHECKING([for thread local storage (TLS) class]) + AC_CACHE_VAL([ac_cv_tls], + [for ax_tls_keyword in __thread '__declspec(thread)' none; do + AS_CASE([$ax_tls_keyword], + [none], [ac_cv_tls=none ; break], + [AC_TRY_COMPILE( + [#include + static void + foo(void) { + static ] $ax_tls_keyword [ int bar; + exit(1); + }], + [], + [ac_cv_tls=$ax_tls_keyword ; break], + ac_cv_tls=none + )]) + done + ]) + AC_MSG_RESULT([$ac_cv_tls]) + + AS_IF([test "$ac_cv_tls" != "none"], + [AC_DEFINE_UNQUOTED([TLS],[$ac_cv_tls],[If the compiler supports a TLS storage class define it to that here]) + m4_ifnblank([$1],[$1])], + [m4_ifnblank([$2],[$2])]) +]) diff --git a/src/c/fastcgi.c b/src/c/fastcgi.c index cda3e1f6..c37debf7 100644 --- a/src/c/fastcgi.c +++ b/src/c/fastcgi.c @@ -1,5 +1,7 @@ #include "config.h" +#include +#include #include #include #include @@ -20,6 +22,8 @@ #include "fastcgi.h" +#define THREAD_LOCAL __thread + extern uw_app uw_application; typedef struct { @@ -44,6 +48,21 @@ typedef struct { int available, used, sock; } FCGI_Input; +// The FastCGI request ID corresponding to the request being handled by the +// current worker thread. (Each worker thread can only handle one request at a +// time.) +static THREAD_LOCAL int current_request_id; + +// Reads the FastCGI request ID from a FastCGI record. The result is guaranteed +// to be in the range [0, 2^16); this function returns an int to avoid C type +// promotion insanity. +static int fastcgi_request_id(const FCGI_Record* const r) { + const int requestid = r->requestIdB1 << 8 | r->requestIdB0; + assert(requestid >= 0); + assert(requestid <= UINT16_MAX); + return requestid; +} + static FCGI_Output *fastcgi_output() { FCGI_Output *o = malloc(sizeof(FCGI_Output)); @@ -70,7 +89,9 @@ static int fastcgi_send(FCGI_Output *o, unsigned char type, unsigned short contentLength) { o->r.type = type; - o->r.requestIdB1 = o->r.requestIdB0 = 0; + assert(current_request_id <= UINT16_MAX); + o->r.requestIdB1 = current_request_id >> 8; + o->r.requestIdB0 = current_request_id & 0x000000ff; o->r.contentLengthB1 = contentLength >> 8; o->r.contentLengthB0 = contentLength & 255; return uw_really_send(o->sock, &o->r, sizeof(o->r) - 65535 + contentLength); @@ -356,6 +377,10 @@ static void *worker(void *data) { goto done; } + // Save the FastCGI request ID this worker is handling so that fastcgi_send + // can include it in its response. + current_request_id = fastcgi_request_id(r); + if (r->type != FCGI_BEGIN_REQUEST) { write_stderr(out, "First message is not BEGIN_REQUEST\n"); goto done; @@ -373,6 +398,15 @@ static void *worker(void *data) { goto done; } + if (fastcgi_request_id(r) != current_request_id) { + write_stderr(out, + "Ignoring environment variables for request %d (current" + " request has id %d)\n", + fastcgi_request_id(r), + current_request_id); + continue; + } + if (r->type != FCGI_PARAMS) { write_stderr(out, "Expected FCGI_PARAMS but got %d\n", r->type); goto done; @@ -428,6 +462,15 @@ static void *worker(void *data) { goto done; } + if (fastcgi_request_id(r) != current_request_id) { + write_stderr(out, + "Ignoring STDIN for request %d (current request has id" + " %d)\n", + fastcgi_request_id(r), + current_request_id); + continue; + } + if (r->type != FCGI_STDIN) { write_stderr(out, "Expected FCGI_STDIN but got %d\n", r->type); goto done; -- cgit v1.2.3 From 730b627fb1cad24b454bc8c3b652ae5bbd21a839 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Wed, 30 Nov 2016 22:13:57 +0100 Subject: use the ip4 socket address for ipv4 again --- src/c/http.c | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/c/http.c b/src/c/http.c index d186e209..afa5de66 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -322,19 +322,27 @@ static void sigint(int signum) { exit(0); } +union uw_sockaddr { + struct sockaddr sa; + struct sockaddr_in ipv4; + struct sockaddr_in6 ipv6; +}; + int main(int argc, char *argv[]) { // The skeleton for this function comes from Beej's sockets tutorial. int sockfd; // listen on sock_fd - struct sockaddr_in6 my_addr; - struct sockaddr_in6 their_addr; // connector's address information + union uw_sockaddr my_addr; + union uw_sockaddr their_addr; // connector's address information socklen_t sin_size; - int yes = 1, no = 0, uw_port = 8080, nthreads = 1, i, *names, opt; + int yes = 1, uw_port = 8080, nthreads = 1, i, *names, opt; int recv_timeout_sec = 5; signal(SIGINT, sigint); signal(SIGPIPE, SIG_IGN); - my_addr.sin6_addr = in6addr_any; // auto-fill with my IP + // default if not specified: IPv4 with my IP + my_addr.sa.sa_family = AF_INET; + my_addr.ipv4.sin_addr.s_addr = INADDR_ANY; // auto-fill with my IP while ((opt = getopt(argc, argv, "hp:a:A:t:kqT:")) != -1) { switch (opt) { @@ -357,20 +365,17 @@ int main(int argc, char *argv[]) { break; case 'a': - { - char *buf = alloca(strlen(optarg) + 8); - strcpy(buf, "::FFFF:"); - strcpy(buf + 7, optarg); - if (!inet_pton(AF_INET6, buf, &my_addr.sin6_addr)) { - fprintf(stderr, "Invalid IPv4 address\n"); - help(argv[0]); - return 1; - } + my_addr.sa.sa_family = AF_INET; + if (!inet_pton(AF_INET, optarg, &my_addr.ipv4.sin_addr)) { + fprintf(stderr, "Invalid IPv4 address\n"); + help(argv[0]); + return 1; } break; case 'A': - if (!inet_pton(AF_INET6, optarg, &my_addr.sin6_addr)) { + my_addr.sa.sa_family = AF_INET6; + if (!inet_pton(AF_INET6, optarg, &my_addr.ipv6.sin6_addr)) { fprintf(stderr, "Invalid IPv6 address\n"); help(argv[0]); return 1; @@ -413,7 +418,7 @@ int main(int argc, char *argv[]) { names = calloc(nthreads, sizeof(int)); - sockfd = socket(AF_INET6, SOCK_STREAM, 0); // do some error checking! + sockfd = socket(my_addr.sa.sa_family, SOCK_STREAM, 0); // do some error checking! if (sockfd < 0) { fprintf(stderr, "Listener socket creation failed\n"); @@ -425,15 +430,13 @@ int main(int argc, char *argv[]) { return 1; } - if (setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &no, sizeof(int)) < 0) { - fprintf(stderr, "Listener IPV6_V6ONLY option resetting failed\n"); - return 1; + switch (my_addr.sa.sa_family) + { + case AF_INET: my_addr.ipv4.sin_port = htons(uw_port); break; + case AF_INET6: my_addr.ipv6.sin6_port = htons(uw_port); break; } - my_addr.sin6_family = AF_INET6; // host byte order - my_addr.sin6_port = htons(uw_port); // short, network byte order - - if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof my_addr) < 0) { + if (bind(sockfd, &my_addr.sa, sizeof my_addr) < 0) { fprintf(stderr, "Listener socket bind failed\n"); return 1; } @@ -470,7 +473,7 @@ int main(int argc, char *argv[]) { } while (1) { - int new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); + int new_fd = accept(sockfd, &their_addr.sa, &sin_size); if (new_fd < 0) { qfprintf(stderr, "Socket accept failed\n"); -- cgit v1.2.3 From 18876ba1e372323b3ed0361bf1d33bd7a22b097e Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Thu, 1 Dec 2016 08:29:51 +0100 Subject: zero initialize the socket address structure --- src/c/http.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/c/http.c b/src/c/http.c index afa5de66..f7fdc122 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -341,6 +341,7 @@ int main(int argc, char *argv[]) { signal(SIGPIPE, SIG_IGN); // default if not specified: IPv4 with my IP + memset(&my_addr, 0, sizeof my_addr); my_addr.sa.sa_family = AF_INET; my_addr.ipv4.sin_addr.s_addr = INADDR_ANY; // auto-fill with my IP -- cgit v1.2.3 From 40191906aa4b640838496174af7c594039bd6fd5 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Thu, 1 Dec 2016 12:52:53 +0100 Subject: pass the proper size to the bind call --- src/c/http.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/c/http.c b/src/c/http.c index f7fdc122..f2d3a4c5 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -333,7 +333,7 @@ int main(int argc, char *argv[]) { int sockfd; // listen on sock_fd union uw_sockaddr my_addr; union uw_sockaddr their_addr; // connector's address information - socklen_t sin_size; + socklen_t my_size = 0, sin_size; int yes = 1, uw_port = 8080, nthreads = 1, i, *names, opt; int recv_timeout_sec = 5; @@ -433,11 +433,18 @@ int main(int argc, char *argv[]) { switch (my_addr.sa.sa_family) { - case AF_INET: my_addr.ipv4.sin_port = htons(uw_port); break; - case AF_INET6: my_addr.ipv6.sin6_port = htons(uw_port); break; + case AF_INET: + /*my_addr.ipv4.sin_len =*/ my_size = sizeof(my_addr.ipv4); + my_addr.ipv4.sin_port = htons(uw_port); + break; + + case AF_INET6: + /*my_addr.ipv6.sin6_len =*/ my_size = sizeof(my_addr.ipv6); + my_addr.ipv6.sin6_port = htons(uw_port); + break; } - if (bind(sockfd, &my_addr.sa, sizeof my_addr) < 0) { + if (bind(sockfd, &my_addr.sa, my_size) < 0) { fprintf(stderr, "Listener socket bind failed\n"); return 1; } -- cgit v1.2.3 From b3a56ca200a9657aceb32886e9108b732d609892 Mon Sep 17 00:00:00 2001 From: Marvin Sielenkemper Date: Thu, 1 Dec 2016 17:34:18 +0100 Subject: remove unnecessary code --- src/c/http.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/c/http.c b/src/c/http.c index f2d3a4c5..1bc58677 100644 --- a/src/c/http.c +++ b/src/c/http.c @@ -434,12 +434,12 @@ int main(int argc, char *argv[]) { switch (my_addr.sa.sa_family) { case AF_INET: - /*my_addr.ipv4.sin_len =*/ my_size = sizeof(my_addr.ipv4); + my_size = sizeof(my_addr.ipv4); my_addr.ipv4.sin_port = htons(uw_port); break; case AF_INET6: - /*my_addr.ipv6.sin6_len =*/ my_size = sizeof(my_addr.ipv6); + my_size = sizeof(my_addr.ipv6); my_addr.ipv6.sin6_port = htons(uw_port); break; } -- cgit v1.2.3 From 245eb671d45ceda8715b8850c7a5c4540da685fa Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 31 Dec 2016 14:27:55 -0500 Subject: Allow qualified variable references in record literals --- src/urweb.grm | 20 ++++++++++++-------- tests/qualrecord.ur | 7 +++++++ 2 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 tests/qualrecord.ur diff --git a/src/urweb.grm b/src/urweb.grm index 40101056..c1ee74f2 100644 --- a/src/urweb.grm +++ b/src/urweb.grm @@ -475,6 +475,7 @@ fun patternOut (e : exp) = | eterm of exp | etuple of exp list | rexp of (con * exp) list * bool + | rpath of con | xml of exp | xmlOne of exp | xmlOpt of exp @@ -1151,15 +1152,15 @@ ctuple : capps STAR capps ([capps1, capps2]) | capps STAR ctuple (capps :: ctuple) rcon : ([]) - | ident EQ cexp ([(ident, cexp)]) - | ident EQ cexp COMMA rcon ((ident, cexp) :: rcon) + | rpath EQ cexp ([(rpath, cexp)]) + | rpath EQ cexp COMMA rcon ((rpath, cexp) :: rcon) -rconn : ident ([(ident, (CUnit, s (identleft, identright)))]) - | ident COMMA rconn ((ident, (CUnit, s (identleft, identright))) :: rconn) +rconn : rpath ([(rpath, (CUnit, s (rpathleft, rpathright)))]) + | rpath COMMA rconn ((rpath, (CUnit, s (rpathleft, rpathright))) :: rconn) rcone : ([]) - | ident COLON cexp ([(ident, cexp)]) - | ident COLON cexp COMMA rcone ((ident, cexp) :: rcone) + | rpath COLON cexp ([(rpath, cexp)]) + | rpath COLON cexp COMMA rcone ((rpath, cexp) :: rcone) ident : CSYMBOL (CName CSYMBOL, s (CSYMBOLleft, CSYMBOLright)) | INT (CName (Int64.toString INT), s (INTleft, INTright)) @@ -1567,8 +1568,11 @@ ptuple : pat COMMA pat ([pat1, pat2]) | pat COMMA ptuple (pat :: ptuple) rexp : DOTDOTDOT ([], true) - | ident EQ eexp ([(ident, eexp)], false) - | ident EQ eexp COMMA rexp ((ident, eexp) :: #1 rexp, #2 rexp) + | rpath EQ eexp ([(rpath, eexp)], false) + | rpath EQ eexp COMMA rexp ((rpath, eexp) :: #1 rexp, #2 rexp) + +rpath : path (CVar path, s (pathleft, pathright)) + | CSYMBOL (CName CSYMBOL, s (CSYMBOLleft, CSYMBOLright)) xml : xmlOne xml (let val pos = s (xmlOneleft, xmlright) diff --git a/tests/qualrecord.ur b/tests/qualrecord.ur new file mode 100644 index 00000000..4db64e5f --- /dev/null +++ b/tests/qualrecord.ur @@ -0,0 +1,7 @@ +structure M = struct + con the_best_name = #Wiggles + con the_runner_up = #Beppo +end + +val x : {M.the_best_name : int, A : int, M.the_runner_up : int} = + {M.the_best_name = 8, A = 9, M.the_runner_up = 10} -- cgit v1.2.3 From 7882613062c5423c3001ab2923509b94c7a5ff22 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 31 Dec 2016 14:50:55 -0500 Subject: Proofread and tweak new demo prose --- demo/prose | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/demo/prose b/demo/prose index e3d6185a..781eeed5 100644 --- a/demo/prose +++ b/demo/prose @@ -1,8 +1,9 @@ -

Ur/Web is a domain-specific language for programming web applications backed by SQL databases. It is (strongly) statically-typed (like ML and Haskell) and purely functional (like Haskell). Ur is the base language, and the web-specific features of Ur/Web (mostly) come only in the form of special rules for parsing and optimization. The Ur core looks a lot like Standard ML, with a few Haskell-isms added, and kinder, gentler versions added of many features from dependently-typed languages like the logic behind Coq. The type system is much more expressive than in ML and Haskell, such that well-typed web applications cannot "go wrong," not just in handling single HTTP requests, but across their entire lifetimes of interacting with HTTP clients. Beyond that, Ur is unusual in using ideas from dependent typing to enable very effective metaprogramming, or programming with explicit analysis of type structure. Many common web application components can be built by Ur/Web functions that operate on types, where it seems impossible to achieve similar code re-use in more established statically-typed languages.

+

Ur/Web is a domain-specific language for programming web applications backed by SQL databases. It is (strongly) statically typed (like ML and Haskell) and purely functional (like Haskell). Ur is the base language, and the web-specific features of Ur/Web (mostly) come only in the form of special rules for parsing and optimization. The Ur core looks a lot like Standard ML, with a few Haskell-isms added, and kinder, gentler versions added of many features from dependently typed languages like the logic behind Coq. The type system is much more expressive than in ML and Haskell, such that well-typed web applications cannot "go wrong," not just in handling single HTTP requests, but across their entire lifetimes of interacting with HTTP clients. Beyond that, Ur is unusual in using ideas from dependent typing to enable very effective metaprogramming, or programming with explicit analysis of type structure. Many common web application components can be built by Ur/Web functions that operate on types, where it seems impossible to achieve similar code re-use in more established statically typed languages.

-

The page you are currently reading is a part of the demo included with the Ur/Web sources and supporting files available from github. The following steps will build a local instance of the demo if you're lucky (and running a debian based linux OS). If you're not lucky, you can consult the beginning of the manual for more detailed instructions.

+

The page you are currently reading is a part of the demo included with the Ur/Web sources and supporting files available from GitHub. The following steps will build a local instance of the demo if you're lucky (and running a Debian-based Linux OS, which actually tend to have Ur/Web packages built in these days). If you're not lucky, you can consult the beginning of the manual for more detailed instructions.

Install System Dependencies
+

sudo apt-get install build-essential \
   emacs-goodies-el \
@@ -13,69 +14,75 @@
   mlton \
   sqlite3

-
Build and install the Ur/web Framework
+
Build and Install the Ur/Web Framework
+

./configure
 make
 sudo make install
 

-
Compile the demo the easy way
+
Compile the Demo the Easy Way
+

$ urweb -dbms sqlite -db /path_to_db.sqlite -demo /Demo demo
 

-

The -dbms sqlite flag indicates that instead of using the default database management system of postgres, we wish to use sqlite (not good for production). The -db flag allows us to specify the path to our sqlite database on the file system. The -demo /Demo parameter indicates that we want to build a demo application that expects its URIs to begin with /Demo. The final argument demo gives the path to a directory housing Ur/Web source files (.ur, .urp, .urs, etc, etc). +

The -dbms sqlite flag indicates that instead of using the default database management system (PostgreSQL), we wish to use SQLite (usually unsuited for production). The -db flag allows us to specify the file-system path to our SQLite database. The -demo /Demo parameter indicates that we want to build a demo application that expects its URIs to begin with /Demo. The final argument demo gives the path to a directory housing Ur/Web source files (.ur, .urp, .urs, etc.).

-The following files are created during the compile process: +The following files are created during the compilation process:

    -
  • demo/demo.exe -
  • demo/out/* -
  • demo/demo.sql +
  • demo/demo.exe +
  • demo/out/* +
  • demo/demo.sql

-
Migrate the Database
+
Initialize the Database
+

-When we compiled the demo in the above step, a demo.sql file was created for us which contains all the information required to create a database congruent with the demo web app. The below command will provision our sqlite database. To see an example of where a database table is defined in source code, check out demo/crud1.ur. Also of interest is the file demo.urp which contains a database line with the PostgreSQL database that the demo web server will try to connect to if database information isn't provided as command line arguments when the webapp is compiled. +When we compiled the demo in the last step, a demo.sql file was created for us, which contains all the information required to create a database compatible with the demo web app. The command below will provision our SQLite database. To see an example of where a database table is defined in source code, check out demo/crud1.ur. Also of interest is the file demo.urp, which contains a database directive with the PostgreSQL database that the demo web server will try to connect to if database information isn't provided as command-line arguments when the application is compiled.

$ sqlite3 /path/to/database/file <demo/demo.sql
 

-
Boot the web app
-Executing the binary generated above (demo/demo.exe) with no arguments will start a single-threaded server listening on port 8080. Pass the flag -h to see which options are available on such freshly built binaries. +
Boot the App
+ +Executing the binary generated above (demo/demo.exe) with no arguments will start a single-threaded server listening on port 8080. (To answer the usual first question: the .exe prefix has nothing to do with Windows and does not mean that you compiled for the wrong OS!) Pass the flag -h to see which options are available on such freshly built binaries.

$ demo/demo.exe
 Database connection initialized.
 Listening on port 8080....
 
-Test out http://localhost:8080/Demo/Demo/main which should consist of links to the individual demos after booting the web app.

+Test out http://localhost:8080/Demo/Demo/main, which should consist of links to the individual demos after booting the app.

Serve the Static Content with a Reverse Proxy
-

The -demo version also generates some HTML in a subdirectory out of the demo directory (eg index.html). It is easy to set Apache up to serve these HTML files, and to proxy out to the Ur/Web web server for dynamic page requests. This configuration works for me, where DIR is the location of an Ur/Web source distribution. + +

The -demo version also generates some HTML in a subdirectory out of the demo directory (e.g. index.html). It is easy to set Apache up to serve these HTML files and to proxy out to the Ur/Web web server for dynamic page requests. This configuration works for me, where DIR is the location of an Ur/Web source distribution. (You may also need to enable the proxy module with a command like a2enmod proxy_http.)

Alias /demo/ "DIR/demo/out/"
 
 ProxyPass /Demo/ http://localhost:8080/Demo/
 ProxyPassReverse /Demo/ http://localhost:8080/Demo/

-
Compile Individually
+

These project files can also be built separately. For example, you could run

$ urweb demo/hello
 
-to build the "Hello World" demo application. Doing so will invite urweb to seak out the various demo/hello.* files and, from them, build a binary demo/demo.exe. The URL to access the resulting webapp will be http://localhost:8080/Hello/main. +to build the "Hello World" demo application. Doing so will invite Ur/Web to seek out the various demo/hello.* files and, from them, build a binary demo/hello.exe. The URL to access the resulting app will be http://localhost:8080/Hello/main.

This File
-

One of the files in the demo directory is named prose, a file describing the different demo pieces with HTML. Some lines of prose have the form foo.urp, naming particular project files (with the extension .urp) in that directory. These makeup the different pages of the tutorial.

+

One of the files in the demo directory is named prose, a file describing the different demo pieces with HTML. Some lines of prose have the form foo.urp, naming particular project files (with the extension .urp) in that directory. These make up the different pages of the tutorial.

+ +
Finally, the Demos!
-
Proceeding Demo Exhibits
-

The rest of the demo focuses on the individual facets of the Ur/ Web framework. Follow the links in the lefthand frame to visit the applications, commentary, and syntax-highlighted source code. (An Emacs mode is behind the syntax highlighting.) I recommend visiting the applications in the order listed, since that is the order in which new concepts are introduced.

+

The rest of the demo focuses on introducing Ur/Web programming, one feature at a time. Follow the links in the lefthand frame to visit the applications, commentary, and syntax-highlighted source code. (An Emacs mode is behind the syntax highlighting.) I recommend visiting the applications in the order listed, since that is the order in which new concepts are introduced.

hello.urp @@ -161,7 +168,7 @@ ref.urp

This example shows how to mix the module system with SQL to implement a kind of "abstract data type." The functor RefFun.Make takes in a type belonging to the type class of those types that may be included in SQL. The functor output includes an abstract type ref, along with operations for working with refs via transactions. In the functor implementation, we see that ref is implemented as int, treated as primary keys of a SQL table.

-

The functor creates a new encapsulated SQL sequence and table on each call. These local relations show up in the automatically-generated SQL file that should be run to prepare the database for use, but they are invisible from client code. We could change the functor to create different SQL relations, without needing to change client code.

+

The functor creates a new encapsulated SQL sequence and table on each call. These local relations show up in the automatically generated SQL file that should be run to prepare the database for use, but they are invisible from client code. We could change the functor to create different SQL relations, without needing to change client code.

Note that, in ref.ur, the inj components of functor arguments are omitted. Since these arguments are type class witnesses, the compiler infers them automatically based on the choices of data.

-- cgit v1.2.3 From 10c0bc3c889568717ce97b6d3bda000b3fd46227 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 31 Dec 2016 15:10:08 -0500 Subject: Proper ordering when libraries are included recursively (fixes #56) --- src/compiler.sml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler.sml b/src/compiler.sml index 4fe2dfd5..481f04b6 100644 --- a/src/compiler.sml +++ b/src/compiler.sml @@ -687,7 +687,7 @@ fun parseUrp' accLibs fname = } in if accLibs then - foldr (fn (job', job) => merge (job, job')) job (!libs) + foldl (fn (job', job) => merge (job, job')) job (!libs) else job end -- cgit v1.2.3 From 923cc0fcad46eae0a00f7d5c8ea39e627b1aaa57 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 31 Dec 2016 15:54:06 -0500 Subject: Use 'id' attribute for
--- lib/ur/basis.urs | 3 ++- src/monoize.sml | 21 ++++++++++++++++----- src/urweb.grm | 35 ++++++++++++++++++++--------------- tests/formid.ur | 9 +++++++++ tests/formid.urs | 1 + 5 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 tests/formid.ur create mode 100644 tests/formid.urs diff --git a/lib/ur/basis.urs b/lib/ur/basis.urs index 8b0d4faa..82d8f6e4 100644 --- a/lib/ur/basis.urs +++ b/lib/ur/basis.urs @@ -947,7 +947,8 @@ val img : bodyTag ([Alt = string, Src = url, Width = int, Height = int, val form : ctx ::: {Unit} -> bind ::: {Type} -> [[MakeForm, Form] ~ ctx] => - option css_class + option id + -> option css_class -> xml ([Form] ++ ctx) [] bind -> xml ([MakeForm] ++ ctx) [] [] diff --git a/src/monoize.sml b/src/monoize.sml index 86f2b4a5..ddf6cd4c 100644 --- a/src/monoize.sml +++ b/src/monoize.sml @@ -3657,9 +3657,10 @@ fun monoExp (env, st, fm) (all as (e, loc)) = end | L.EApp ( - (L.EApp ((L.ECApp ( - (L.ECApp ((L.EFfi ("Basis", "form"), _), _), _), - (L.CRecord (_, fields), _)), _), + (L.EApp ((L.EApp ((L.ECApp ( + (L.ECApp ((L.EFfi ("Basis", "form"), _), _), _), + (L.CRecord (_, fields), _)), _), + id), _), class), _), xml) => let @@ -3793,6 +3794,7 @@ fun monoExp (env, st, fm) (all as (e, loc)) = action val stt = (L'.TFfi ("Basis", "string"), loc) + val (id, fm) = monoExp (env, st, fm) id val (class, fm) = monoExp (env, st, fm) class val action = (L'.EStrcat (action, (L'.ECase (class, @@ -3806,8 +3808,17 @@ fun monoExp (env, st, fm) (all as (e, loc)) = result = stt}), loc)), loc) in ((L'.EStrcat ((L'.EStrcat (strH ""), loc)), loc), + (L'.EStrcat ((L'.ECase (id, + [((L'.PNone stt, loc), + strH ""), + ((L'.PSome (stt, (L'.PVar ("id", stt), loc)), loc), + (L'.EStrcat (strH " id=\"", + (L'.EStrcat ((L'.ERel 0, loc), + strH "\""), loc)), loc))], + {disc = (L'.TOption stt, loc), + result = stt}), loc), + (L'.EStrcat (action, + strH ">"), loc)), loc)), loc), (L'.EStrcat (xml, strH "
"), loc)), loc), fm) diff --git a/src/urweb.grm b/src/urweb.grm index c1ee74f2..db5473a6 100644 --- a/src/urweb.grm +++ b/src/urweb.grm @@ -479,7 +479,7 @@ fun patternOut (e : exp) = | xml of exp | xmlOne of exp | xmlOpt of exp - | tag of (string * exp) * exp option * exp option * exp + | tag of (string * exp) * exp option * exp option * exp option * exp | tagHead of string * exp | bind of pat * con option * exp | edecl of edecl @@ -500,7 +500,7 @@ fun patternOut (e : exp) = | rpat of (string * pat) list * bool | ptuple of pat list - | attrs of exp option * exp option * exp option * exp option * (string * string * exp) list * (con * exp) list + | attrs of exp option * exp option * exp option * exp option * exp option * (string * string * exp) list * (con * exp) list | attr of attr | attrv of exp @@ -1610,7 +1610,7 @@ xmlOne : NOTAGS (EApp ((EVar (["Basis"], "cdata", Infer) (EPrim (Prim.String (Prim.Html, "")), pos)), pos) in - (EApp (#4 tag, cdata), pos) + (EApp (#5 tag, cdata), pos) end) | tag GT xmlOpt END_TAG (let @@ -1626,6 +1626,9 @@ xmlOne : NOTAGS (EApp ((EVar (["Basis"], "cdata", Infer) if et = "form" then let val e = (EVar (["Basis"], "form", Infer), pos) + val e = (EApp (e, case #4 tag of + NONE => (EVar (["Basis"], "None", Infer), pos) + | SOME c => (EApp ((EVar (["Basis"], "Some", Infer), pos), c), pos)), pos) val e = (EApp (e, case #2 tag of NONE => (EVar (["Basis"], "None", Infer), pos) | SOME (EPrim (Prim.String (_, s)), _) => (EApp ((EVar (["Basis"], "Some", Infer), pos), parseClass s pos), pos) @@ -1643,7 +1646,7 @@ xmlOne : NOTAGS (EApp ((EVar (["Basis"], "cdata", Infer) (EApp ((EVar (["Basis"], "entry", Infer), pos), xmlOpt), pos) else - (EApp (#4 tag, xmlOpt), pos) + (EApp (#5 tag, xmlOpt), pos) else (if ErrorMsg.anyErrors () then () @@ -1688,8 +1691,8 @@ tag : tagHead attrs (let e), pos) val e = (EApp (e, eo), pos) - val atts = case #5 attrs of - [] => #6 attrs + val atts = case #6 attrs of + [] => #7 attrs | data :: datas => let fun doOne (kind, name, value) = @@ -1709,14 +1712,14 @@ tag : tagHead attrs (let (EApp (e, doOne nv), pos) end) (doOne data) datas in - ((CName "Data", pos), datas') :: #6 attrs + ((CName "Data", pos), datas') :: #7 attrs end val e = (EApp (e, (ERecord (atts, false), pos)), pos) val e = (EApp (e, (EApp (#2 tagHead, (ERecord ([], false), pos)), pos)), pos) in - (tagHead, #1 attrs, #2 attrs, e) + (tagHead, #1 attrs, #2 attrs, #5 attrs, e) end) tagHead: BEGIN_TAG (let @@ -1728,7 +1731,7 @@ tagHead: BEGIN_TAG (let end) | tagHead LBRACE cexp RBRACE (#1 tagHead, (ECApp (#2 tagHead, cexp), s (tagHeadleft, RBRACEright))) -attrs : (NONE, NONE, NONE, NONE, [], []) +attrs : (NONE, NONE, NONE, NONE, NONE, [], []) | attr attrs (let val loc = s (attrleft, attrsright) in @@ -1737,26 +1740,28 @@ attrs : (NONE, NONE, NONE, NONE, [], []) (case #1 attrs of NONE => () | SOME _ => ErrorMsg.errorAt loc "Multiple classes specified for tag"; - (SOME e, #2 attrs, #3 attrs, #4 attrs, #5 attrs, #6 attrs)) + (SOME e, #2 attrs, #3 attrs, #4 attrs, #5 attrs, #6 attrs, #7 attrs)) | DynClass e => (case #2 attrs of NONE => () | SOME _ => ErrorMsg.errorAt loc "Multiple dynamic classes specified for tag"; - (#1 attrs, SOME e, #3 attrs, #4 attrs, #5 attrs, #6 attrs)) + (#1 attrs, SOME e, #3 attrs, #4 attrs, #5 attrs, #6 attrs, #7 attrs)) | Style e => (case #3 attrs of NONE => () | SOME _ => ErrorMsg.errorAt loc "Multiple styles specified for tag"; - (#1 attrs, #2 attrs, SOME e, #4 attrs, #5 attrs, #6 attrs)) + (#1 attrs, #2 attrs, SOME e, #4 attrs, #5 attrs, #6 attrs, #7 attrs)) | DynStyle e => (case #4 attrs of NONE => () | SOME _ => ErrorMsg.errorAt loc "Multiple dynamic classes specified for tag"; - (#1 attrs, #2 attrs, #3 attrs, SOME e, #5 attrs, #6 attrs)) + (#1 attrs, #2 attrs, #3 attrs, SOME e, #5 attrs, #6 attrs, #7 attrs)) | Data xe => - (#1 attrs, #2 attrs, #3 attrs, #4 attrs, xe :: #5 attrs, #6 attrs) + (#1 attrs, #2 attrs, #3 attrs, #4 attrs, #5 attrs, xe :: #6 attrs, #7 attrs) | Normal xe => - (#1 attrs, #2 attrs, #3 attrs, #4 attrs, #5 attrs, xe :: #6 attrs) + (#1 attrs, #2 attrs, #3 attrs, #4 attrs, (case #1 (#1 xe) of + CName "Id" => SOME (#2 xe) + | _ => #5 attrs), #6 attrs, xe :: #7 attrs) end) attr : SYMBOL EQ attrv (case SYMBOL of diff --git a/tests/formid.ur b/tests/formid.ur new file mode 100644 index 00000000..c9e3317d --- /dev/null +++ b/tests/formid.ur @@ -0,0 +1,9 @@ +fun handler () = return + +fun main () : transaction page = + id <- fresh; + return +
+ + +
diff --git a/tests/formid.urs b/tests/formid.urs new file mode 100644 index 00000000..6ac44e0b --- /dev/null +++ b/tests/formid.urs @@ -0,0 +1 @@ +val main : unit -> transaction page -- cgit v1.2.3 From b6ef142f8950b1b5271b68f18ec44f38f88b5851 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Sat, 31 Dec 2016 20:33:43 -0500 Subject: Update test to track last change --- tests/crud1.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/crud1.html b/tests/crud1.html index 92cd1942..b1f34b54 100644 --- a/tests/crud1.html +++ b/tests/crud1.html @@ -24,8 +24,7 @@


- -
+
  • A:
  • B:
  • C:
  • -- cgit v1.2.3 From 4e1f94952d1f8b861e52decddcadbbdc4f83d32f Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Mon, 2 Jan 2017 11:04:34 -0500 Subject: Add 'placeholder' attribute for textareas --- lib/ur/basis.urs | 4 ++-- tests/textarea_placeholder.ur | 12 ++++++++++++ tests/textarea_placeholder.urs | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 tests/textarea_placeholder.ur create mode 100644 tests/textarea_placeholder.urs diff --git a/lib/ur/basis.urs b/lib/ur/basis.urs index 82d8f6e4..23896e27 100644 --- a/lib/ur/basis.urs +++ b/lib/ur/basis.urs @@ -982,7 +982,7 @@ val hidden : formTag string [] [Data = data_attr, Id = string, Value = string] val textbox : formTag string [] ([Value = string, Size = int, Placeholder = string, Source = source string, Onchange = transaction unit, Ontext = transaction unit] ++ boxAttrs ++ inputAttrs) val password : formTag string [] ([Value = string, Size = int, Placeholder = string, Onchange = transaction unit] ++ boxAttrs ++ inputAttrs) -val textarea : formTag string [] ([Rows = int, Cols = int, Onchange = transaction unit, +val textarea : formTag string [] ([Rows = int, Cols = int, Placeholder = string, Onchange = transaction unit, Ontext = transaction unit] ++ boxAttrs ++ inputAttrs) val checkbox : formTag bool [] ([Checked = bool, Onchange = transaction unit] ++ boxAttrs) @@ -1092,7 +1092,7 @@ val ccheckbox : cformTag ([Size = int, Source = source bool, Onchange = transact val cselect : cformTag ([Source = source string, Onchange = transaction unit] ++ boxAttrs) [Cselect] val coption : unit -> tag [Value = string, Selected = bool] [Cselect, Body] [] [] [] -val ctextarea : cformTag ([Rows = int, Cols = int, Source = source string, Onchange = transaction unit, +val ctextarea : cformTag ([Rows = int, Cols = int, Placeholder = string, Source = source string, Onchange = transaction unit, Ontext = transaction unit] ++ boxAttrs ++ inputAttrs) [] (*** Tables *) diff --git a/tests/textarea_placeholder.ur b/tests/textarea_placeholder.ur new file mode 100644 index 00000000..b328f838 --- /dev/null +++ b/tests/textarea_placeholder.ur @@ -0,0 +1,12 @@ +fun lame _ = return + +fun main () = + s <- source ""; + return + + + + + + + diff --git a/tests/textarea_placeholder.urs b/tests/textarea_placeholder.urs new file mode 100644 index 00000000..6ac44e0b --- /dev/null +++ b/tests/textarea_placeholder.urs @@ -0,0 +1 @@ +val main : unit -> transaction page -- cgit v1.2.3 From 4bbdbbc72d96567f8c5a1d435beef32d447dec30 Mon Sep 17 00:00:00 2001 From: Adam Chlipala Date: Thu, 5 Jan 2017 14:34:52 -0500 Subject: New release --- CHANGELOG | 9 +++++++++ configure.ac | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 20d79590..89fee4f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +======== +20170105 +======== + +- Allow qualified variable references in record literals +- Add 'placeholder' attribute for textareas +- Add more explicit build instructions to main demo +- Bug fixes + ======== 20161022 ======== diff --git a/configure.ac b/configure.ac index 0dda6d3b..c87b37ed 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ -AC_INIT([urweb], [20161022]) -WORKING_VERSION=1 +AC_INIT([urweb], [20170105]) +WORKING_VERSION=0 AC_USE_SYSTEM_EXTENSIONS # automake 1.12 requires this, but automake 1.11 doesn't recognize it -- cgit v1.2.3