ziv@2235: structure Sqlcache (* DEBUG: add back :> SQLCACHE. *) = struct ziv@2209: ziv@2209: open Mono ziv@2209: ziv@2209: structure IS = IntBinarySet ziv@2209: structure IM = IntBinaryMap ziv@2213: structure SK = struct type ord_key = string val compare = String.compare end ziv@2213: structure SS = BinarySetFn(SK) ziv@2213: structure SM = BinaryMapFn(SK) ziv@2213: structure SIMM = MultimapFn(structure KeyMap = SM structure ValSet = IS) ziv@2209: ziv@2216: (* Filled in by [cacheWrap] during [Sqlcache]. *) ziv@2213: val ffiInfo : {index : int, params : int} list ref = ref [] ziv@2209: ziv@2227: fun resetFfiInfo () = ffiInfo := [] ziv@2227: ziv@2213: fun getFfiInfo () = !ffiInfo ziv@2213: ziv@2215: (* Some FFIs have writing as their only effect, which the caching records. *) ziv@2215: val ffiEffectful = ziv@2223: (* ASK: how can this be less hard-coded? *) ziv@2215: let ziv@2215: val fs = SS.fromList ["htmlifyInt_w", ziv@2215: "htmlifyFloat_w", ziv@2215: "htmlifyString_w", ziv@2215: "htmlifyBool_w", ziv@2215: "htmlifyTime_w", ziv@2215: "attrifyInt_w", ziv@2215: "attrifyFloat_w", ziv@2215: "attrifyString_w", ziv@2215: "attrifyChar_w", ziv@2215: "urlifyInt_w", ziv@2215: "urlifyFloat_w", ziv@2215: "urlifyString_w", ziv@2215: "urlifyBool_w", ziv@2215: "urlifyChannel_w"] ziv@2215: in ziv@2215: fn (m, f) => Settings.isEffectful (m, f) ziv@2215: andalso not (m = "Basis" andalso SS.member (fs, f)) ziv@2215: end ziv@2215: ziv@2234: val cache = ref LruCache.cache ziv@2233: fun setCache c = cache := c ziv@2233: fun getCache () = !cache ziv@2233: ziv@2215: ziv@2215: (* Effect analysis. *) ziv@2215: ziv@2216: (* Makes an exception for [EWrite] (which is recorded when caching). *) ziv@2230: fun effectful doPrint (effs : IS.set) (inFunction : bool) (bound : int) : exp -> bool = ziv@2215: (* If result is true, expression is definitely effectful. If result is ziv@2215: false, then expression is definitely not effectful if effs is fully ziv@2215: populated. The intended pattern is to use this a number of times equal ziv@2215: to the number of declarations in a file, Bellman-Ford style. *) ziv@2234: (* TODO: make incrementing of the number of bound variables cleaner, ziv@2234: probably by using [MonoUtil] instead of all this. *) ziv@2215: let ziv@2215: (* DEBUG: remove printing when done. *) ziv@2215: fun tru msg = if doPrint then (print (msg ^ "\n"); true) else true ziv@2215: val rec eff' = ziv@2215: (* ASK: is there a better way? *) ziv@2215: fn EPrim _ => false ziv@2215: (* We don't know if local functions have effects when applied. *) ziv@2215: | ERel idx => if inFunction andalso idx >= bound ziv@2215: then tru ("rel" ^ Int.toString idx) else false ziv@2215: | ENamed name => if IS.member (effs, name) then tru "named" else false ziv@2215: | ECon (_, _, NONE) => false ziv@2215: | ECon (_, _, SOME e) => eff e ziv@2215: | ENone _ => false ziv@2215: | ESome (_, e) => eff e ziv@2215: | EFfi (m, f) => if ffiEffectful (m, f) then tru "ffi" else false ziv@2215: | EFfiApp (m, f, _) => if ffiEffectful (m, f) then tru "ffiapp" else false ziv@2215: (* ASK: we're calling functions effectful if they have effects when ziv@2215: applied or if the function expressions themselves have effects. ziv@2215: Is that okay? *) ziv@2215: (* This is okay because the values we ultimately care about aren't ziv@2215: functions, and this is a conservative approximation, anyway. *) ziv@2215: | EApp (eFun, eArg) => effectful doPrint effs true bound eFun orelse eff eArg ziv@2215: | EAbs (_, _, _, e) => effectful doPrint effs inFunction (bound+1) e ziv@2215: | EUnop (_, e) => eff e ziv@2215: | EBinop (_, _, e1, e2) => eff e1 orelse eff e2 ziv@2215: | ERecord xs => List.exists (fn (_, e, _) => eff e) xs ziv@2215: | EField (e, _) => eff e ziv@2215: (* If any case could be effectful, consider it effectful. *) ziv@2215: | ECase (e, xs, _) => eff e orelse List.exists (fn (_, e) => eff e) xs ziv@2215: | EStrcat (e1, e2) => eff e1 orelse eff e2 ziv@2215: (* ASK: how should we treat these three? *) ziv@2215: | EError _ => tru "error" ziv@2215: | EReturnBlob _ => tru "blob" ziv@2215: | ERedirect _ => tru "redirect" ziv@2215: (* EWrite is a special exception because we record writes when caching. *) ziv@2215: | EWrite _ => false ziv@2215: | ESeq (e1, e2) => eff e1 orelse eff e2 ziv@2215: (* TODO: keep context of which local variables aren't effectful? Only ziv@2215: makes a difference for function expressions, though. *) ziv@2215: | ELet (_, _, eBind, eBody) => eff eBind orelse ziv@2215: effectful doPrint effs inFunction (bound+1) eBody ziv@2215: | EClosure (_, es) => List.exists eff es ziv@2215: (* TODO: deal with EQuery. *) ziv@2215: | EQuery _ => tru "query" ziv@2215: | EDml _ => tru "dml" ziv@2215: | ENextval _ => tru "nextval" ziv@2215: | ESetval _ => tru "setval" ziv@2215: | EUnurlify (e, _, _) => eff e ziv@2215: (* ASK: how should we treat this? *) ziv@2215: | EJavaScript _ => tru "javascript" ziv@2215: (* ASK: these are all effectful, right? *) ziv@2215: | ESignalReturn _ => tru "signalreturn" ziv@2215: | ESignalBind _ => tru "signalbind" ziv@2215: | ESignalSource _ => tru "signalsource" ziv@2215: | EServerCall _ => tru "servercall" ziv@2215: | ERecv _ => tru "recv" ziv@2215: | ESleep _ => tru "sleep" ziv@2215: | ESpawn _ => tru "spawn" ziv@2215: and eff = fn (e', _) => eff' e' ziv@2215: in ziv@2215: eff ziv@2215: end ziv@2215: ziv@2215: (* TODO: test this. *) ziv@2215: val effectfulMap = ziv@2215: let ziv@2215: fun doVal ((_, name, _, e, _), effMap) = ziv@2215: if effectful false effMap false 0 e ziv@2215: then IS.add (effMap, name) ziv@2215: else effMap ziv@2215: val doDecl = ziv@2215: fn (DVal v, effMap) => doVal (v, effMap) ziv@2215: (* Repeat the list of declarations a number of times equal to its size. *) ziv@2215: | (DValRec vs, effMap) => ziv@2215: List.foldl doVal effMap (List.concat (List.map (fn _ => vs) vs)) ziv@2215: (* ASK: any other cases? *) ziv@2215: | (_, effMap) => effMap ziv@2215: in ziv@2215: MonoUtil.File.fold {typ = #2, exp = #2, decl = doDecl} IS.empty ziv@2215: end ziv@2215: ziv@2215: ziv@2216: (* Boolean formula normalization. *) ziv@2216: ziv@2234: datatype junctionType = Conj | Disj ziv@2216: ziv@2216: datatype 'atom formula = ziv@2216: Atom of 'atom ziv@2216: | Negate of 'atom formula ziv@2234: | Combo of junctionType * 'atom formula list ziv@2216: ziv@2234: val flipJt = fn Conj => Disj | Disj => Conj ziv@2216: ziv@2235: fun listBind xs f = List.concat (map f xs) ziv@2216: ziv@2216: val rec cartesianProduct : 'a list list -> 'a list list = ziv@2216: fn [] => [[]] ziv@2235: | (xs :: xss) => listBind (cartesianProduct xss) ziv@2235: (fn ys => listBind xs (fn x => [x :: ys])) ziv@2216: ziv@2218: (* Pushes all negation to the atoms.*) ziv@2218: fun pushNegate (negate : 'atom -> 'atom) (negating : bool) = ziv@2218: fn Atom x => Atom (if negating then negate x else x) ziv@2218: | Negate f => pushNegate negate (not negating) f ziv@2234: | Combo (n, fs) => Combo (if negating then flipJt n else n, map (pushNegate negate negating) fs) ziv@2218: ziv@2218: val rec flatten = ziv@2235: fn Combo (_, [f]) => flatten f ziv@2235: | Combo (j, fs) => ziv@2235: Combo (j, List.foldr (fn (f, acc) => ziv@2218: case f of ziv@2235: Combo (j', fs') => ziv@2235: if j = j' orelse length fs' = 1 ziv@2235: then fs' @ acc ziv@2235: else f :: acc ziv@2218: | _ => f :: acc) ziv@2218: [] ziv@2218: (map flatten fs)) ziv@2218: | f => f ziv@2218: ziv@2235: fun normalize' ((simplifyLists, simplifyAtomsConj, simplifyAtomsDisj, negate) ziv@2235: : ('a list list -> 'a list list) ziv@2235: * ('a list -> 'a list) ziv@2235: * ('a list -> 'a list) ziv@2235: * ('a -> 'a)) ziv@2235: (junc : junctionType) = ziv@2216: let ziv@2235: fun simplify junc = simplifyLists o map (case junc of ziv@2235: Conj => simplifyAtomsConj ziv@2235: | Disj => simplifyAtomsDisj) ziv@2235: fun norm junc = ziv@2235: simplify junc ziv@2235: o (fn Atom x => [[x]] ziv@2235: | Negate f => map (map negate) (norm (flipJt junc) f) ziv@2235: | Combo (j, fs) => ziv@2235: let ziv@2235: val fss = listBind fs (norm j) ziv@2235: in ziv@2235: if j = junc then fss else cartesianProduct fss ziv@2235: end) ziv@2216: in ziv@2235: norm junc ziv@2216: end ziv@2216: ziv@2235: fun normalize (simplifyLists, simplifyAtomsConj, simplifyAtomsDisj, negate, junc) = ziv@2235: (normalize' (simplifyLists, simplifyAtomsConj, simplifyAtomsDisj, negate) junc) ziv@2235: o flatten ziv@2235: o pushNegate negate false ziv@2216: ziv@2221: fun mapFormula mf = ziv@2221: fn Atom x => Atom (mf x) ziv@2221: | Negate f => Negate (mapFormula mf f) ziv@2235: | Combo (j, fs) => Combo (j, map (mapFormula mf) fs) ziv@2216: ziv@2230: ziv@2215: (* SQL analysis. *) ziv@2213: ziv@2235: structure CmpKey : ORD_KEY = struct ziv@2235: ziv@2235: type ord_key = Sql.cmp ziv@2235: ziv@2235: val compare = ziv@2235: fn (Sql.Eq, Sql.Eq) => EQUAL ziv@2235: | (Sql.Eq, _) => LESS ziv@2235: | (_, Sql.Eq) => GREATER ziv@2235: | (Sql.Ne, Sql.Ne) => EQUAL ziv@2235: | (Sql.Ne, _) => LESS ziv@2235: | (_, Sql.Ne) => GREATER ziv@2235: | (Sql.Lt, Sql.Lt) => EQUAL ziv@2235: | (Sql.Lt, _) => LESS ziv@2235: | (_, Sql.Lt) => GREATER ziv@2235: | (Sql.Le, Sql.Le) => EQUAL ziv@2235: | (Sql.Le, _) => LESS ziv@2235: | (_, Sql.Le) => GREATER ziv@2235: | (Sql.Gt, Sql.Gt) => EQUAL ziv@2235: | (Sql.Gt, _) => LESS ziv@2235: | (_, Sql.Gt) => GREATER ziv@2235: | (Sql.Ge, Sql.Ge) => EQUAL ziv@2235: ziv@2235: end ziv@2235: ziv@2235: ziv@2235: functor ListKeyFn (K : ORD_KEY) : ORD_KEY = struct ziv@2235: ziv@2235: type ord_key = K.ord_key list ziv@2235: ziv@2235: val rec compare = ziv@2235: fn ([], []) => EQUAL ziv@2235: | ([], _) => LESS ziv@2235: | (_, []) => GREATER ziv@2235: | (x :: xs, y :: ys) => (case K.compare (x, y) of ziv@2235: EQUAL => compare (xs, ys) ziv@2235: | ord => ord) ziv@2235: ziv@2235: end ziv@2235: ziv@2235: functor OptionKeyFn (K : ORD_KEY) : ORD_KEY = struct ziv@2235: ziv@2235: type ord_key = K.ord_key option ziv@2235: ziv@2235: val compare = ziv@2235: fn (NONE, NONE) => EQUAL ziv@2235: | (NONE, _) => LESS ziv@2235: | (_, NONE) => GREATER ziv@2235: | (SOME x, SOME y) => K.compare (x, y) ziv@2235: ziv@2235: end ziv@2235: ziv@2235: functor TripleKeyFn (structure I : ORD_KEY ziv@2235: structure J : ORD_KEY ziv@2235: structure K : ORD_KEY) ziv@2235: : ORD_KEY where type ord_key = I.ord_key * J.ord_key * K.ord_key = struct ziv@2235: ziv@2235: type ord_key = I.ord_key * J.ord_key * K.ord_key ziv@2235: ziv@2235: fun compare ((i1, j1, k1), (i2, j2, k2)) = ziv@2235: case I.compare (i1, i2) of ziv@2235: EQUAL => (case J.compare (j1, j2) of ziv@2235: EQUAL => K.compare (k1, k2) ziv@2235: | ord => ord) ziv@2235: | ord => ord ziv@2235: ziv@2235: end ziv@2235: ziv@2216: val rec chooseTwos : 'a list -> ('a * 'a) list = ziv@2216: fn [] => [] ziv@2216: | x :: ys => map (fn y => (x, y)) ys @ chooseTwos ys ziv@2213: ziv@2216: datatype atomExp = ziv@2216: QueryArg of int ziv@2216: | DmlRel of int ziv@2216: | Prim of Prim.t ziv@2216: | Field of string * string ziv@2216: ziv@2216: structure AtomExpKey : ORD_KEY = struct ziv@2216: ziv@2234: type ord_key = atomExp ziv@2216: ziv@2234: val compare = ziv@2234: fn (QueryArg n1, QueryArg n2) => Int.compare (n1, n2) ziv@2234: | (QueryArg _, _) => LESS ziv@2234: | (_, QueryArg _) => GREATER ziv@2234: | (DmlRel n1, DmlRel n2) => Int.compare (n1, n2) ziv@2234: | (DmlRel _, _) => LESS ziv@2234: | (_, DmlRel _) => GREATER ziv@2234: | (Prim p1, Prim p2) => Prim.compare (p1, p2) ziv@2234: | (Prim _, _) => LESS ziv@2234: | (_, Prim _) => GREATER ziv@2234: | (Field (t1, f1), Field (t2, f2)) => ziv@2234: case String.compare (t1, t2) of ziv@2234: EQUAL => String.compare (f1, f2) ziv@2234: | ord => ord ziv@2216: ziv@2216: end ziv@2216: ziv@2216: structure UF = UnionFindFn(AtomExpKey) ziv@2234: ziv@2235: structure ConflictMaps = struct ziv@2235: ziv@2235: structure TK = TripleKeyFn(structure I = CmpKey ziv@2235: structure J = OptionKeyFn(AtomExpKey) ziv@2235: structure K = OptionKeyFn(AtomExpKey)) ziv@2235: structure TS = BinarySetFn(TK) ziv@2235: structure TLS = BinarySetFn(ListKeyFn(TK)) ziv@2235: ziv@2235: val toKnownEquality = ziv@2235: (* [NONE] here means unkown. Anything that isn't a comparison between two ziv@2235: knowns shouldn't be used, and simply dropping unused terms is okay in ziv@2235: disjunctive normal form. *) ziv@2235: fn (Sql.Eq, SOME e1, SOME e2) => SOME (e1, e2) ziv@2235: | _ => NONE ziv@2235: ziv@2235: val equivClasses : (Sql.cmp * atomExp option * atomExp option) list -> atomExp list list = ziv@2235: UF.classes ziv@2235: o List.foldl UF.union' UF.empty ziv@2235: o List.mapPartial toKnownEquality ziv@2235: ziv@2235: fun addToEqs (eqs, n, e) = ziv@2235: case IM.find (eqs, n) of ziv@2235: (* Comparing to a constant is probably better than comparing to a ziv@2235: variable? Checking that existing constants match a new ones is ziv@2235: handled by [accumulateEqs]. *) ziv@2235: SOME (Prim _) => eqs ziv@2235: | _ => IM.insert (eqs, n, e) ziv@2235: ziv@2235: val accumulateEqs = ziv@2235: (* [NONE] means we have a contradiction. *) ziv@2235: fn (_, NONE) => NONE ziv@2235: | ((Prim p1, Prim p2), eqso) => ziv@2235: (case Prim.compare (p1, p2) of ziv@2235: EQUAL => eqso ziv@2235: | _ => NONE) ziv@2235: | ((QueryArg n, Prim p), SOME eqs) => SOME (addToEqs (eqs, n, Prim p)) ziv@2235: | ((QueryArg n, DmlRel r), SOME eqs) => SOME (addToEqs (eqs, n, DmlRel r)) ziv@2235: | ((Prim p, QueryArg n), SOME eqs) => SOME (addToEqs (eqs, n, Prim p)) ziv@2235: | ((DmlRel r, QueryArg n), SOME eqs) => SOME (addToEqs (eqs, n, DmlRel r)) ziv@2235: (* TODO: deal with equalities between [DmlRel]s and [Prim]s. ziv@2235: This would involve guarding the invalidation with a check for the ziv@2235: relevant comparisons. *) ziv@2235: (* DEBUG: remove these print statements. *) ziv@2235: (* | ((DmlRel r, Prim p), eqso) => (print ("sadness " ^ Int.toString r ^ " = " ^ Prim.toString p ^ "\n"); eqso) *) ziv@2235: (* | ((Prim p, DmlRel r), eqso) => (print ("sadness " ^ Int.toString r ^ " = " ^ Prim.toString p ^ "\n"); eqso) *) ziv@2235: | (_, eqso) => eqso ziv@2235: ziv@2235: val eqsOfClass : atomExp list -> atomExp IM.map option = ziv@2235: List.foldl accumulateEqs (SOME IM.empty) ziv@2235: o chooseTwos ziv@2235: ziv@2235: fun toAtomExps rel (cmp, e1, e2) = ziv@2235: let ziv@2235: val qa = ziv@2235: (* Here [NONE] means unkown. *) ziv@2235: fn Sql.SqConst p => SOME (Prim p) ziv@2235: | Sql.Field tf => SOME (Field tf) ziv@2235: | Sql.Inj (EPrim p, _) => SOME (Prim p) ziv@2235: | Sql.Inj (ERel n, _) => SOME (rel n) ziv@2235: (* We can't deal with anything else, e.g., CURRENT_TIMESTAMP ziv@2235: becomes Sql.Unmodeled, which becomes NONE here. *) ziv@2235: | _ => NONE ziv@2235: in ziv@2235: (cmp, qa e1, qa e2) ziv@2235: end ziv@2235: ziv@2235: fun negateCmp (cmp, e1, e2) = ziv@2235: (case cmp of ziv@2235: Sql.Eq => Sql.Ne ziv@2235: | Sql.Ne => Sql.Eq ziv@2235: | Sql.Lt => Sql.Ge ziv@2235: | Sql.Le => Sql.Gt ziv@2235: | Sql.Gt => Sql.Le ziv@2235: | Sql.Ge => Sql.Lt, ziv@2235: e1, e2) ziv@2235: ziv@2235: val markQuery : (Sql.cmp * Sql.sqexp * Sql.sqexp) formula -> ziv@2235: (Sql.cmp * atomExp option * atomExp option) formula = ziv@2235: mapFormula (toAtomExps QueryArg) ziv@2235: ziv@2235: val markDml : (Sql.cmp * Sql.sqexp * Sql.sqexp) formula -> ziv@2235: (Sql.cmp * atomExp option * atomExp option) formula = ziv@2235: mapFormula (toAtomExps DmlRel) ziv@2235: (* No eqs should have key conflicts because no variable is in two ziv@2235: equivalence classes, so the [#1] could be [#2]. *) ziv@2235: ziv@2235: val mergeEqs : (atomExp IntBinaryMap.map option list ziv@2235: -> atomExp IntBinaryMap.map option) = ziv@2235: List.foldr (fn (SOME eqs, SOME acc) => SOME (IM.unionWith #1 (eqs, acc)) | _ => NONE) ziv@2235: (SOME IM.empty) ziv@2235: ziv@2235: fun dnf (fQuery, fDml) = ziv@2235: let ziv@2235: val isStar = ziv@2235: (* TODO: decide if this is okay and, if so, factor out magic ziv@2235: string "*" to a common location. *) ziv@2235: (* First guess: definitely okay for conservative approximation, ziv@2235: though information lost might be useful even in current ziv@2235: implementation for finding an extra equality. *) ziv@2235: fn SOME (Field (_, field)) => String.isSuffix "*" field ziv@2235: | _ => false ziv@2235: fun canIgnore (_, a1, a2) = isStar a1 orelse isStar a2 ziv@2235: fun simplifyLists xs = TLS.listItems (TLS.addList (TLS.empty, xs)) ziv@2235: fun simplifyAtomsConj xs = TS.listItems (TS.addList (TS.empty, xs)) ziv@2235: val simplifyAtomsDisj = simplifyAtomsConj o List.filter canIgnore ziv@2235: in ziv@2235: normalize (simplifyLists, simplifyAtomsConj, simplifyAtomsDisj, negateCmp, Disj) ziv@2235: (Combo (Conj, [markQuery fQuery, markDml fDml])) ziv@2235: end ziv@2235: ziv@2235: val conflictMaps = List.mapPartial (mergeEqs o map eqsOfClass o equivClasses) o dnf ziv@2235: ziv@2235: end ziv@2235: ziv@2235: val conflictMaps = ConflictMaps.conflictMaps ziv@2213: ziv@2216: val rec sqexpToFormula = ziv@2234: fn Sql.SqTrue => Combo (Conj, []) ziv@2234: | Sql.SqFalse => Combo (Disj, []) ziv@2216: | Sql.SqNot e => Negate (sqexpToFormula e) ziv@2216: | Sql.Binop (Sql.RCmp c, e1, e2) => Atom (c, e1, e2) ziv@2234: | Sql.Binop (Sql.RLop l, p1, p2) => Combo (case l of Sql.And => Conj | Sql.Or => Disj, ziv@2216: [sqexpToFormula p1, sqexpToFormula p2]) ziv@2216: (* ASK: any other sqexps that can be props? *) ziv@2216: | _ => raise Match ziv@2213: ziv@2218: fun renameTables tablePairs = ziv@2216: let ziv@2216: fun renameString table = ziv@2216: case List.find (fn (_, t) => table = t) tablePairs of ziv@2216: NONE => table ziv@2216: | SOME (realTable, _) => realTable ziv@2216: val renameSqexp = ziv@2216: fn Sql.Field (table, field) => Sql.Field (renameString table, field) ziv@2216: | e => e ziv@2218: fun renameAtom (cmp, e1, e2) = (cmp, renameSqexp e1, renameSqexp e2) ziv@2216: in ziv@2218: mapFormula renameAtom ziv@2216: end ziv@2218: ziv@2218: val rec queryToFormula = ziv@2234: fn Sql.Query1 {Where = NONE, ...} => Combo (Conj, []) ziv@2218: | Sql.Query1 {From = tablePairs, Where = SOME e, ...} => ziv@2218: renameTables tablePairs (sqexpToFormula e) ziv@2234: | Sql.Union (q1, q2) => Combo (Disj, [queryToFormula q1, queryToFormula q2]) ziv@2216: ziv@2218: fun valsToFormula (table, vals) = ziv@2234: Combo (Conj, map (fn (field, v) => Atom (Sql.Eq, Sql.Field (table, field), v)) vals) ziv@2218: ziv@2216: val rec dmlToFormula = ziv@2221: fn Sql.Insert (table, vals) => valsToFormula (table, vals) ziv@2218: | Sql.Delete (table, wher) => renameTables [(table, "T")] (sqexpToFormula wher) ziv@2218: | Sql.Update (table, vals, wher) => ziv@2218: let ziv@2221: val fWhere = sqexpToFormula wher ziv@2221: val fVals = valsToFormula (table, vals) ziv@2221: (* TODO: don't use field name hack. *) ziv@2221: val markField = ziv@2221: fn Sql.Field (t, v) => Sql.Field (t, v ^ "*") ziv@2221: | e => e ziv@2221: val mark = mapFormula (fn (cmp, e1, e2) => (cmp, markField e1, markField e2)) ziv@2218: in ziv@2218: renameTables [(table, "T")] ziv@2234: (Combo (Disj, [Combo (Conj, [fVals, mark fWhere]), ziv@2234: Combo (Conj, [mark fVals, fWhere])])) ziv@2218: end ziv@2213: ziv@2213: val rec tablesQuery = ziv@2216: fn Sql.Query1 {From = tablePairs, ...} => SS.fromList (map #1 tablePairs) ziv@2216: | Sql.Union (q1, q2) => SS.union (tablesQuery q1, tablesQuery q2) ziv@2213: ziv@2213: val tableDml = ziv@2216: fn Sql.Insert (tab, _) => tab ziv@2216: | Sql.Delete (tab, _) => tab ziv@2216: | Sql.Update (tab, _, _) => tab ziv@2213: ziv@2213: ziv@2213: (* Program instrumentation. *) ziv@2213: ziv@2234: val varName = ziv@2234: let ziv@2234: val varNumber = ref 0 ziv@2234: in ziv@2234: fn s => (varNumber := !varNumber + 1; s ^ Int.toString (!varNumber)) ziv@2234: end ziv@2234: ziv@2233: val {check, store, flush, ...} = getCache () ziv@2233: ziv@2230: val dummyLoc = ErrorMsg.dummySpan ziv@2216: ziv@2230: fun stringExp s = (EPrim (Prim.String (Prim.Normal, s)), dummyLoc) ziv@2230: ziv@2230: val stringTyp = (TFfi ("Basis", "string"), dummyLoc) ziv@2213: ziv@2213: val sequence = ziv@2213: fn (exp :: exps) => ziv@2213: let ziv@2230: val loc = dummyLoc ziv@2213: in ziv@2213: List.foldl (fn (e', seq) => ESeq ((seq, loc), (e', loc))) exp exps ziv@2213: end ziv@2213: | _ => raise Match ziv@2213: ziv@2215: (* Always increments negative indices because that's what we need later. *) ziv@2215: fun incRelsBound bound inc = ziv@2215: MonoUtil.Exp.mapB ziv@2215: {typ = fn x => x, ziv@2215: exp = fn level => ziv@2215: (fn ERel n => ERel (if n >= level orelse n < 0 then n + inc else n) ziv@2215: | x => x), ziv@2215: bind = fn (level, MonoUtil.Exp.RelE _) => level + 1 | (level, _) => level} ziv@2215: bound ziv@2215: ziv@2215: val incRels = incRelsBound 0 ziv@2213: ziv@2223: fun cacheWrap (query, i, urlifiedRel0, resultTyp, args) = ziv@2213: let ziv@2223: val () = ffiInfo := {index = i, params = length args} :: !ffiInfo ziv@2230: val loc = dummyLoc ziv@2223: (* We ensure before this step that all arguments aren't effectful. ziv@2227: by turning them into local variables as needed. *) ziv@2230: val argsInc = map (incRels 1) args ziv@2233: val check = (check (i, args), dummyLoc) ziv@2233: val store = (store (i, argsInc, urlifiedRel0), dummyLoc) ziv@2223: val rel0 = (ERel 0, loc) ziv@2213: in ziv@2223: ECase (check, ziv@2223: [((PNone stringTyp, loc), ziv@2234: (ELet (varName "q", resultTyp, query, (ESeq (store, rel0), loc)), loc)), ziv@2234: ((PSome (stringTyp, (PVar (varName "hit", stringTyp), loc)), loc), ziv@2223: (* Boolean is false because we're not unurlifying from a cookie. *) ziv@2223: (EUnurlify (rel0, resultTyp, false), loc))], ziv@2223: {disc = stringTyp, result = resultTyp}) ziv@2213: end ziv@2213: ziv@2213: fun fileMapfold doExp file start = ziv@2213: case MonoUtil.File.mapfold {typ = Search.return2, ziv@2213: exp = fn x => (fn s => Search.Continue (doExp x s)), ziv@2213: decl = Search.return2} file start of ziv@2213: Search.Continue x => x ziv@2213: | Search.Return _ => raise Match ziv@2213: ziv@2213: fun fileMap doExp file = #1 (fileMapfold (fn x => fn _ => (doExp x, ())) file ()) ziv@2213: ziv@2221: fun factorOutNontrivial text = ziv@2221: let ziv@2230: val loc = dummyLoc ziv@2221: fun strcat (e1, e2) = (EStrcat (e1, e2), loc) ziv@2221: val chunks = Sql.chunkify text ziv@2221: val (newText, newVariables) = ziv@2221: (* Important that this is foldr (to oppose foldl below). *) ziv@2221: List.foldr ziv@2221: (fn (chunk, (qText, newVars)) => ziv@2221: (* Variable bound to the head of newBs will have the lowest index. *) ziv@2221: case chunk of ziv@2221: Sql.Exp (e as (EPrim _, _)) => (strcat (e, qText), newVars) ziv@2221: | Sql.Exp e => ziv@2221: let ziv@2221: val n = length newVars ziv@2221: in ziv@2221: (* This is the (n + 1)th new variable, so there are ziv@2221: already n new variables bound, so we increment ziv@2221: indices by n. *) ziv@2221: (strcat ((ERel (~(n+1)), loc), qText), incRels n e :: newVars) ziv@2221: end ziv@2221: | Sql.String s => (strcat (stringExp s, qText), newVars)) ziv@2221: (stringExp "", []) ziv@2221: chunks ziv@2221: fun wrapLets e' = ziv@2221: (* Important that this is foldl (to oppose foldr above). *) ziv@2234: List.foldl (fn (v, e') => ELet (varName "sqlArg", stringTyp, v, (e', loc))) ziv@2221: e' ziv@2221: newVariables ziv@2221: val numArgs = length newVariables ziv@2221: in ziv@2221: (newText, wrapLets, numArgs) ziv@2221: end ziv@2221: ziv@2215: fun addChecking file = ziv@2213: let ziv@2223: fun doExp (queryInfo as (tableToIndices, indexToQueryNumArgs, index)) = ziv@2223: fn e' as EQuery {query = origQueryText, ziv@2223: sqlcacheInfo = urlifiedRel0, ziv@2223: state = resultTyp, ziv@2223: initial, body, tables, exps} => ziv@2213: let ziv@2221: val (newQueryText, wrapLets, numArgs) = factorOutNontrivial origQueryText ziv@2215: (* Increment once for each new variable just made. *) ziv@2221: val queryExp = incRels numArgs ziv@2215: (EQuery {query = newQueryText, ziv@2223: sqlcacheInfo = urlifiedRel0, ziv@2223: state = resultTyp, ziv@2215: initial = initial, ziv@2215: body = body, ziv@2215: tables = tables, ziv@2223: exps = exps}, ziv@2230: dummyLoc) ziv@2215: val (EQuery {query = queryText, ...}, _) = queryExp ziv@2235: (* DEBUG *) ziv@2221: val () = Print.preface ("sqlcache> ", (MonoPrint.p_exp MonoEnv.empty queryText)) ziv@2230: val args = List.tabulate (numArgs, fn n => (ERel n, dummyLoc)) ziv@2213: fun bind x f = Option.mapPartial f x ziv@2215: fun guard b x = if b then x else NONE ziv@2215: (* DEBUG: set first boolean argument to true to turn on printing. *) ziv@2215: fun safe bound = not o effectful true (effectfulMap file) false bound ziv@2213: val attempt = ziv@2213: (* Ziv misses Haskell's do notation.... *) ziv@2215: guard (safe 0 queryText andalso safe 0 initial andalso safe 2 body) ( ziv@2216: bind (Sql.parse Sql.query queryText) (fn queryParsed => ziv@2223: SOME (wrapLets (cacheWrap (queryExp, index, urlifiedRel0, resultTyp, args)), ziv@2218: (SS.foldr (fn (tab, qi) => SIMM.insert (qi, tab, index)) ziv@2218: tableToIndices ziv@2218: (tablesQuery queryParsed), ziv@2223: IM.insert (indexToQueryNumArgs, index, (queryParsed, numArgs)), ziv@2223: index + 1)))) ziv@2213: in ziv@2213: case attempt of ziv@2213: SOME pair => pair ziv@2213: | NONE => (e', queryInfo) ziv@2213: end ziv@2213: | e' => (e', queryInfo) ziv@2213: in ziv@2223: fileMapfold (fn exp => fn state => doExp state exp) file (SIMM.empty, IM.empty, 0) ziv@2213: end ziv@2213: ziv@2235: structure Invalidations = struct ziv@2235: ziv@2235: val loc = dummyLoc ziv@2235: ziv@2235: val optionAtomExpToExp = ziv@2235: fn NONE => (ENone stringTyp, loc) ziv@2235: | SOME e => (ESome (stringTyp, ziv@2235: (case e of ziv@2235: DmlRel n => ERel n ziv@2235: | Prim p => EPrim p ziv@2235: (* TODO: make new type containing only these two. *) ziv@2235: | _ => raise Match, ziv@2235: loc)), ziv@2235: loc) ziv@2235: ziv@2235: fun eqsToInvalidation numArgs eqs = ziv@2235: let ziv@2235: fun inv n = if n < 0 then [] else IM.find (eqs, n) :: inv (n - 1) ziv@2235: in ziv@2235: inv (numArgs - 1) ziv@2235: end ziv@2235: ziv@2235: (* Tests if [ys] makes [xs] a redundant cache invalidation. [NONE] here ziv@2235: represents unknown, which means a wider invalidation. *) ziv@2235: val rec madeRedundantBy : atomExp option list * atomExp option list -> bool = ziv@2235: fn ([], []) => true ziv@2235: | (NONE :: xs, _ :: ys) => madeRedundantBy (xs, ys) ziv@2235: | (SOME x :: xs, SOME y :: ys) => (case AtomExpKey.compare (x, y) of ziv@2235: EQUAL => madeRedundantBy (xs, ys) ziv@2235: | _ => false) ziv@2235: | _ => false ziv@2235: ziv@2235: fun removeRedundant' (xss, yss) = ziv@2235: case xss of ziv@2235: [] => yss ziv@2235: | xs :: xss' => ziv@2235: removeRedundant' (xss', ziv@2235: if List.exists (fn ys => madeRedundantBy (xs, ys)) (xss' @ yss) ziv@2235: then yss ziv@2235: else xs :: yss) ziv@2235: ziv@2235: fun removeRedundant xss = removeRedundant' (xss, []) ziv@2235: ziv@2235: fun eqss (query, dml) = conflictMaps (queryToFormula query, dmlToFormula dml) ziv@2235: ziv@2235: fun invalidations ((query, numArgs), dml) = ziv@2235: (map (map optionAtomExpToExp) ziv@2235: o removeRedundant ziv@2235: o map (eqsToInvalidation numArgs) ziv@2235: o eqss) ziv@2235: (query, dml) ziv@2235: ziv@2235: end ziv@2235: ziv@2235: val invalidations = Invalidations.invalidations ziv@2235: ziv@2235: (* DEBUG *) ziv@2235: val gunk : ((Sql.query * int) * Sql.dml) list ref = ref [] ziv@2216: ziv@2223: fun addFlushing (file, (tableToIndices, indexToQueryNumArgs, _)) = ziv@2213: let ziv@2221: val flushes = List.concat o ziv@2233: map (fn (i, argss) => map (fn args => flush (i, args)) argss) ziv@2213: val doExp = ziv@2221: fn EDml (origDmlText, failureMode) => ziv@2213: let ziv@2221: val (newDmlText, wrapLets, numArgs) = factorOutNontrivial origDmlText ziv@2221: val dmlText = incRels numArgs newDmlText ziv@2221: val dmlExp = EDml (dmlText, failureMode) ziv@2235: (* DEBUG *) ziv@2221: val () = Print.preface ("sqlcache> ", (MonoPrint.p_exp MonoEnv.empty dmlText)) ziv@2221: val invs = ziv@2216: case Sql.parse Sql.dml dmlText of ziv@2218: SOME dmlParsed => ziv@2221: map (fn i => (case IM.find (indexToQueryNumArgs, i) of ziv@2221: SOME queryNumArgs => ziv@2235: (* DEBUG *) ziv@2235: (gunk := (queryNumArgs, dmlParsed) :: !gunk; ziv@2235: (i, invalidations (queryNumArgs, dmlParsed))) ziv@2221: (* TODO: fail more gracefully. *) ziv@2221: | NONE => raise Match)) ziv@2221: (SIMM.findList (tableToIndices, tableDml dmlParsed)) ziv@2221: (* TODO: fail more gracefully. *) ziv@2221: | NONE => raise Match ziv@2213: in ziv@2221: wrapLets (sequence (flushes invs @ [dmlExp])) ziv@2213: end ziv@2213: | e' => e' ziv@2213: in ziv@2235: (* DEBUG *) ziv@2235: gunk := []; ziv@2213: fileMap doExp file ziv@2213: end ziv@2213: ziv@2221: val inlineSql = ziv@2221: let ziv@2221: val doExp = ziv@2221: (* TODO: EQuery, too? *) ziv@2221: (* ASK: should this live in [MonoOpt]? *) ziv@2221: fn EDml ((ECase (disc, cases, {disc = dTyp, ...}), loc), failureMode) => ziv@2221: let ziv@2221: val newCases = map (fn (p, e) => (p, (EDml (e, failureMode), loc))) cases ziv@2221: in ziv@2221: ECase (disc, newCases, {disc = dTyp, result = (TRecord [], loc)}) ziv@2221: end ziv@2221: | e => e ziv@2221: in ziv@2221: fileMap doExp ziv@2221: end ziv@2221: ziv@2213: fun go file = ziv@2213: let ziv@2235: (* TODO: do something nicer than [Sql] being in one of two modes. *) ziv@2227: val () = (resetFfiInfo (); Sql.sqlcacheMode := true) ziv@2221: val file' = addFlushing (addChecking (inlineSql file)) ziv@2215: val () = Sql.sqlcacheMode := false ziv@2213: in ziv@2221: file' ziv@2213: end ziv@2213: ziv@2209: end