diff --git a/src/Transform/DeadNTermTagsElim.fram b/src/Transform/DeadNTermTagsElim.fram index 63bc831..ea69298 100644 --- a/src/Transform/DeadNTermTagsElim.fram +++ b/src/Transform/DeadNTermTagsElim.fram @@ -10,8 +10,177 @@ be used. This optimization reduces the number of non-terminals generated by ##} import open Repr/RichGrammar +import open Utils/UID +import List +import Map + +let (Map { module TagMap }) = Map.make { Key = String } +let (Map { module UIDMap }) = Map.make { Key = UID } + +type TagSet = TagMap.T Unit +type MayTags = NTermMap.T TagSet +type VarEnv = UIDMap.T NTermId +type Atom = Pair Tag UID +type Valuation = List (Pair Atom Bool) + +{# This pass assumes a valid RichGrammar: TC_Tag variables must be bound by +PS_NTerm symbols, not by tokens. Missing bindings are invariant violations. #} +let mayTagsOf (mayTags : MayTags) (id : NTermId) = + mayTags.findErr + { ~onError = fn _ => impossible () } + id + +let initMayTags nterms = + List.foldLeft + (fn (mayTags : MayTags) (nt : NTerm) => + mayTags.add nt.id TagMap.empty) + NTermMap.empty + nterms + +let addMayTag (mayTags : MayTags) (id : NTermId) tag = + let tagSet = mayTagsOf mayTags id in + if tagSet.mem tag then + (mayTags, False) + else + (mayTags.add id (tagSet.add tag ()), True) + +let addSymbolVar (varEnv : VarEnv) sym = + match sym with + | PS_Token _ => varEnv + | PS_NTerm {var} id => varEnv.add var.id id + end + +let prodVarEnv (prod : NTermProd) = + List.foldLeft addSymbolVar UIDMap.empty prod.symbols + +let lookupVarNTerm (varEnv : VarEnv) (var : Var) = + varEnv.findErr { ~onError = fn _ => impossible () } var.id + +let atomPossible mayTags varEnv tag var = + let ntermId = lookupVarNTerm varEnv var in + (mayTagsOf mayTags ntermId).mem tag + +let sameAtom ((tag1, id1) : Atom) ((tag2, id2) : Atom) = + tag1 == tag2 && id1 == id2 + +let hasAtom atom atoms = + List.exists (sameAtom atom) atoms + +let addAtom atom atoms = + if hasAtom atom atoms then atoms else atom :: atoms + +let rec collectPossibleAtoms mayTags varEnv cond atoms = + match cond with + | TC_True => atoms + | TC_False => atoms + | TC_Tag tag var => + if atomPossible mayTags varEnv tag var then + addAtom (tag, var.id) atoms + else + atoms + | TC_And cond1 cond2 => + atoms + |> collectPossibleAtoms mayTags varEnv cond1 + |> collectPossibleAtoms mayTags varEnv cond2 + | TC_Or cond1 cond2 => + atoms + |> collectPossibleAtoms mayTags varEnv cond1 + |> collectPossibleAtoms mayTags varEnv cond2 + end + +let rec allValuations atoms = + match atoms with + | [] => [[]] + | atom :: atoms => + allValuations atoms + |> List.concatMap + (fn (valuation : Valuation) => + [ (atom, True) :: valuation + , (atom, False) :: valuation + ]) + end + +let atomValue (valuation : Valuation) atom = + valuation + |> List.findErr + { ~onError = fn _ => impossible () } + (fn ((atom', _) : Pair Atom Bool) => sameAtom atom atom') + |> snd + +let rec evalCond mayTags varEnv valuation cond = + match cond with + | TC_True => True + | TC_False => False + | TC_Tag tag var => + if atomPossible mayTags varEnv tag var then + atomValue valuation (tag, var.id) + else + False + | TC_And cond1 cond2 => + evalCond mayTags varEnv valuation cond1 + && evalCond mayTags varEnv valuation cond2 + | TC_Or cond1 cond2 => + evalCond mayTags varEnv valuation cond1 + || evalCond mayTags varEnv valuation cond2 + end + +let feasible mayTags prod cond = + let varEnv = prodVarEnv prod in + let atoms = + [] + |> collectPossibleAtoms mayTags varEnv cond + |> collectPossibleAtoms mayTags varEnv prod.unless + in + {# Tag conditions are expected to be small. #} + allValuations atoms + |> List.exists + (fn (valuation : Valuation) => + evalCond mayTags varEnv valuation cond + && not (evalCond mayTags varEnv valuation prod.unless)) + +let stepTag ntId prod (mayTags, changed) (tag, cond) = + if feasible mayTags prod cond then + (let (mayTags, tagAdded) = addMayTag mayTags ntId tag in + (mayTags, changed || tagAdded)) + else + (mayTags, changed) + +let stepProd ntId state prod = + List.foldLeft (stepTag ntId prod) state prod.tags + +let stepNTerm state (nt : NTerm) = + List.foldLeft (stepProd nt.id) state nt.prods + +let step nterms mayTags = + List.foldLeft stepNTerm (mayTags, False) nterms + +let rec fixpoint nterms mayTags = + let (mayTags, changed) = step nterms mayTags in + if changed then + fixpoint nterms mayTags + else + mayTags + +let filterTags mayTags prod = + List.filter + (fn ((_, cond) : Pair Tag (TagCond Var)) => + feasible mayTags prod cond) + prod.tags + +let rewriteProd mayTags prod = + let (NTermProd {module Prod}) = prod in + NTermProd { module Prod, tags = filterTags mayTags prod } + +let rewriteNTerm mayTags (NTerm {module NT}) = + NTerm + { module NT + , prods = List.map (rewriteProd mayTags) NT.prods + } {## Eliminate dead tags from non-terminals. ##} -pub let transform (g : RichGrammar) : RichGrammar = - # TODO: Implement this function. - g +pub let transform (RichGrammar {module G}) : RichGrammar = + let mayTags = fixpoint G.nterms (initMayTags G.nterms) in + RichGrammar + { module G + , nterms = List.map (rewriteNTerm mayTags) G.nterms + } diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..5809704 --- /dev/null +++ b/test.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -u + +if [ $# -ne 1 ]; then + echo "USAGE: ./test.sh TEST_SUITE" + exit 1 +fi + +if [ ! -f "$1" ]; then + echo "ERROR: test suite file not found: $1" + exit 1 +fi + +if [ ! -r "$1" ]; then + echo "ERROR: test suite file is not readable: $1" + exit 1 +fi + +binary="${DBL:-dbl}" +if ! command -v "$binary" > /dev/null; then + echo "ERROR: dbl executable not found in PATH" + exit 1 +fi + +if [ -z "${DBL_LIB:-}" ]; then + dbl_path=$(command -v "$binary") + dbl_prefix=$(dirname "$(dirname "$dbl_path")") + if [ -d "$dbl_prefix/lib/dbl/stdlib" ]; then + export DBL_LIB="$dbl_prefix/lib/dbl/stdlib" + else + echo "ERROR: DBL_LIB is not set and DBL stdlib was not found next to '$dbl_path'" + exit 1 + fi +fi + +flags="" +total_tests=0 +passed_tests=0 + +function simple_test { + total_tests=$((total_tests + 1)) + + local file="$1" + local cmd=("$binary") + if [ -n "$flags" ]; then + # shellcheck disable=SC2206 + cmd+=($flags) + fi + cmd+=("$file") + + echo "${cmd[*]}" + if "${cmd[@]}"; then + passed_tests=$((passed_tests + 1)) + else + echo "Test file failed: $file" + fi +} + +function run_with_flags { + local flags="$2" + "$1" +} + +source "$1" + +echo "Passed: ${passed_tests}/${total_tests}" + +if [ "$passed_tests" -eq "$total_tests" ]; then + exit 0 +else + exit 1 +fi diff --git a/test/TestAll.fram b/test/TestAll.fram new file mode 100644 index 0000000..c98d0b4 --- /dev/null +++ b/test/TestAll.fram @@ -0,0 +1 @@ +import TransformTests/DeadNTermTagsElim diff --git a/test/TransformTests/DeadNTermTagsElim.fram b/test/TransformTests/DeadNTermTagsElim.fram new file mode 100644 index 0000000..40f9b1b --- /dev/null +++ b/test/TransformTests/DeadNTermTagsElim.fram @@ -0,0 +1,259 @@ +{# This file is part of FramYard, released under MIT license. + See LICENSE for details. + #} + +import open Testing +import /List +import open Repr/RichGrammar +import Transform/DeadNTermTagsElim +import Utils/UID + +let pos = + Position + { fname = "" + , line = 1 + , cnum = 0 + , bol = 0 + , length = 0 + } + +let action name = ACode {pos, code = name} + +let var name typ = + Var {id = UID.fresh (), name = Some name, typ} + +let tokenSym token = + PS_Token {var = None} token + +let tokenSymVar token v = + PS_Token {var = Some v} token + +let ntermSym id v = + PS_NTerm {var = v, level = LBot} id + +let prod symbols tags unless actionName = + NTermProd + { symbols + , level = LBot + , tags + , unless + , action = action actionName + } + +let tokenProd token tags = + prod [tokenSym token] tags TC_False token + +let mkNTerm id name prods = + NTerm {id, name, valueType = TToken name, prods} + +let mkGrammar nterms = + RichGrammar + { tokens = [] + , datatypes = [] + , typeDefs = [] + , nterms + , treeSymbols = [] + , preambleCode = [] + } + +let allProds (g : RichGrammar) = + List.concatMap (fn (NTerm {prods}) => prods) g.nterms + +let prodHasTag tag (prod : NTermProd) = + List.exists + (fn ((prodTag, _) : Pair Tag (TagCond Var)) => prodTag == tag) + prod.tags + +let countTag tag (g : RichGrammar) = + g + |> allProds + |> List.filter (prodHasTag tag) + |> List.length + +let aliveTagGrammar () = + let nId = NTermId.fresh () in + mkGrammar + [ mkNTerm nId "N" + [ tokenProd "TOKEN" [("alive", TC_True)] ] + ] + +let impossibleArgTagGrammar () = + let argId = NTermId.fresh () in + let nId = NTermId.fresh () in + let x = var "x" (TToken "Arg") in + mkGrammar + [ mkNTerm argId "Arg" + [ tokenProd "ARG" [] ] + , mkNTerm nId "N" + [ prod + [ntermSym argId x] + [("derived", TC_Tag "missing" x)] + TC_False + "derived" + ] + ] + +let fixedPointGrammar () = + let baseId = NTermId.fresh () in + let midId = NTermId.fresh () in + let topId = NTermId.fresh () in + let baseVar = var "base" (TToken "Base") in + let midVar = var "mid" (TToken "Mid") in + mkGrammar + [ mkNTerm baseId "Base" + [ tokenProd "BASE" [("base", TC_True)] ] + , mkNTerm midId "Mid" + [ prod + [ntermSym baseId baseVar] + [("mid", TC_Tag "base" baseVar)] + TC_False + "mid" + ] + , mkNTerm topId "Top" + [ prod + [ntermSym midId midVar] + [("top", TC_Tag "mid" midVar)] + TC_False + "top" + ] + ] + +let unlessKillsTagGrammar () = + let baseId = NTermId.fresh () in + let nId = NTermId.fresh () in + let x = var "x" (TToken "Base") in + mkGrammar + [ mkNTerm baseId "Base" + [ tokenProd "BASE" [("effect", TC_True)] ] + , mkNTerm nId "N" + [ prod + [ntermSym baseId x] + [("effect", TC_Tag "effect" x)] + (TC_Tag "effect" x) + "blocked" + ] + ] + +let manyTagsGrammar () = + let leftId = NTermId.fresh () in + let rightId = NTermId.fresh () in + let nId = NTermId.fresh () in + let left = var "left" (TToken "Left") in + let right = var "right" (TToken "Right") in + mkGrammar + [ mkNTerm leftId "Left" + [ tokenProd "LEFT" [("a", TC_True), ("b", TC_True)] ] + , mkNTerm rightId "Right" + [ tokenProd "RIGHT" [("c", TC_True)] ] + , mkNTerm nId "N" + [ prod + [ ntermSym leftId left + , ntermSym rightId right + ] + [ ( "all" + , TC_And + (TC_Tag "a" left) + (TC_And (TC_Tag "b" left) (TC_Tag "c" right)) + ) + , ( "either" + , TC_Or (TC_Tag "missing" left) (TC_Tag "c" right) + ) + , ( "dead" + , TC_And (TC_Tag "missing" left) (TC_Tag "c" right) + ) + ] + TC_False + "many" + ] + ] + +let splitLevelsDeadTagGrammar () = + let exprId = NTermId.fresh () in + let expr10Id = NTermId.fresh () in + let exprTopId = NTermId.fresh () in + let semiL = var "semi-l" (TToken "Expr") in + let semiR = var "semi-r" (TToken "Expr") in + let fwd = var "fwd" (TToken "Expr") in + let plusL = var "plus-l" (TToken "Expr") in + let plusR = var "plus-r" (TToken "Expr") in + mkGrammar + [ mkNTerm exprId "Expr" + [ prod + [ ntermSym expr10Id semiL + , tokenSym "SEMI" + , ntermSym exprId semiR + ] + [] + TC_False + "semi" + , prod + [ntermSym expr10Id fwd] + [("boxed", TC_Tag "boxed" fwd)] + TC_False + "forward" + ] + , mkNTerm expr10Id "Expr@10" + [ prod + [ ntermSym expr10Id plusL + , tokenSym "PLUS" + , ntermSym exprTopId plusR + ] + [("boxed", TC_Tag "boxed" plusR)] + TC_False + "plus" + , prod + [ntermSym exprTopId (var "top" (TToken "Expr"))] + [] + TC_False + "top-forward" + ] + , mkNTerm exprTopId "Expr@top" + [ tokenProd "INT" [] ] + ] + +let aliveTagResult = + DeadNTermTagsElim.transform (aliveTagGrammar ()) + +let impossibleArgTagResult = + DeadNTermTagsElim.transform (impossibleArgTagGrammar ()) + +let fixedPointResult = + DeadNTermTagsElim.transform (fixedPointGrammar ()) + +let unlessKillsTagResult = + DeadNTermTagsElim.transform (unlessKillsTagGrammar ()) + +let manyTagsResult = + DeadNTermTagsElim.transform (manyTagsGrammar ()) + +let splitLevelsDeadTagResult = + DeadNTermTagsElim.transform (splitLevelsDeadTagGrammar ()) + +let _ = + +testSuite "DeadNTermTagsElim" (fn _ => + + testCase "keeps unconditional live tags" (fn _ => + assertEq 1 (countTag "alive" aliveTagResult)); + + testCase "removes tags conditioned on impossible argument tags" (fn _ => + assertEq 0 (countTag "derived" impossibleArgTagResult)); + + testCase "keeps tags discovered through a fixed point" (fn _ => + expectEq 1 (countTag "base" fixedPointResult); + expectEq 1 (countTag "mid" fixedPointResult); + assertEq 1 (countTag "top" fixedPointResult)); + + testCase "removes tags disabled by unless" (fn _ => + assertEq 1 (countTag "effect" unlessKillsTagResult)); + + testCase "handles several tags in one production" (fn _ => + expectEq 1 (countTag "a" manyTagsResult); + expectEq 1 (countTag "b" manyTagsResult); + expectEq 1 (countTag "c" manyTagsResult); + expectEq 1 (countTag "all" manyTagsResult); + expectEq 1 (countTag "either" manyTagsResult); + assertEq 0 (countTag "dead" manyTagsResult)); + + testCase "removes user and technical dead tags after split levels" (fn _ => + assertEq 0 (countTag "boxed" splitLevelsDeadTagResult))) diff --git a/test/test_suite b/test/test_suite new file mode 100644 index 0000000..38dc532 --- /dev/null +++ b/test/test_suite @@ -0,0 +1,5 @@ +function framyard_tests { + simple_test test/TestAll.fram +} + +run_with_flags framyard_tests "-L src -L test"