# Helper functions for defining 'safe' and 'unsafe' sequences, given a # conversion tranducer and a putative (but functional) inverse. # requires: brackets.foma # Shorthand for composition of conversions, just to make intent clear. define ROUNDTRIP(A,B) A .o. B; # Identify strings for which conversion (via A) is *not* losslessly reversible # (via B). A is the conversion, B is the putative inverse. # ROUNDTRIP is ROUNDTRIP(A,B) # The result does not include the empty string, which is assumed to always # be safe. # Intersects with NO'BRACKETS to assert that BRACKETS are not in the # input alphabet. define NOTSAFE(roundtrip) NO'BRACKETS .o. _notid(roundtrip); # The inverse: regex to identify strings for which conversion (via A) is # losslessly reversible (via B). ROUNDTRIP is ROUNDTRIP(A,B). # ~NOTSAFE does include the empty string; remove it (by intersecting # with \BRACKETS+) so that SAFE matches at least 1 character. define SAFE(roundtrip) [\BRACKETS]+ .o. [~NOTSAFE(roundtrip)]; # The identity conversion is often useful, for example as the inverse conversion # B if we expect that the input if probably already in our desired variant. define IDENTITY ?*; # Check safety of a bracketing function: run round-trip conversion on # unbracketed texts and verify that the result is an identity function. # Use `test null` to verify that the "not ok" set is empty; # use `print shortest-string` to find cases where bracketfunc is broken. define BRACK'NOT'SAFE(roundtrip,bracketfunc) NO'BRACKETS .o. _notid([ bracketfunc .o. SWAPBRACKETS .o. [ [\BRACKETS] | [[ "[[" \BRACKETS* "]]" ] .o. REMOVEBRACKETS .o. roundtrip] ]* ]); # Quicker (but less safe) variant: all notsafe strings should have # brackets in their output. # Use `test null` to verify that the "not ok" set is empty; # use `print shortest-string` to find cases where bracketfunc is broken. define BRACK'NOT'SAFE'FAST(roundtrip,bracketfunc) [ NOTSAFE(roundtrip) & [ bracketfunc & [?:[\BRACKETS]]+ ] ]; # Assert that the bracket and conversion functions # are universal (ie, test upper-universal, but it's broken: # https://github.com/mhulden/foma/issues/74) # The result of TEST'UPPER'UNIVERSAL should be null; if not, it gives the # strings not accepted. define TEST'UPPER'UNIVERSAL(x) ~[[REMOVEBRACKETS .o. x].u]; # Combine these tests! define BRACK'NOT'OK(roundtrip,bracketfunc) [ BRACK'NOT'SAFE(roundtrip,bracketfunc) | TEST'UPPER'UNIVERSAL(bracketfunc) ]; # ------------------------------------------------------------ # Note: removing empty brackets isn't always safe -- they may be required to # break up an otherwise-converted phrase. define REMOVEEMPTYBRACKETS "[[" "]]" -> 0; # Remove empty regions and merge adjacent regions. # This is safe to do on unsafe regions, but its not necessarily safe to # expand safe regions. define EXPANDREGIONS REMOVEEMPTYBRACKETS .o. [ "]]" "[[" -> 0 ]; # Expand regions across break (non-word) characters # (Careful: removing breaks from a safe region may make it unsafe.) define EXPANDREGIONSWORD(BREAKPLUS) EXPANDREGIONS .o. [ "]]" -> 0 || _ BREAKPLUS "[[" ,, "[[" -> 0 || "]]" BREAKPLUS _ ]; # This is a safe way to expand safe regions (only if expanded region is safe) define SAFE'EXPANDREGIONS'foma'bug(safe) [ "[[" [[[\BRACKETS] | ["]]":0 "[[":0]]+ .o. safe] "]]" @-> ]; # There's a foma bug with negated expressions in transducer replacement # like the above, so work around this by doing a standard regexp replacement # to surround the entire region with double double brackets, then remove the # brackets inside. define SAFE'EXPANDREGIONS(safe) [ [ "[[" [[[\BRACKETS] | ["]]":0 "[[":0]]+ .o. safe] "]]" ].u @-> "[[" ... "]]" ] .o. [ "]]" "[[" -> 0 // "[[" "[[" \BRACKETS* _ ] .o. [ "[[" -> 0 || "[[" _ ,, "]]" -> 0 || "]]" _ ]; # ------------------------------------------------------------ # Simplest possible bracketing function: brackets entire string if unsafe. define SIMPLEBRACK(safe) NO'BRACKETS .o. [ safe -> "[[" ... "]]" || .#. _ .#. ] .o. SWAPBRACKETS; define SIMPLEBRACK'NOT'OK(roundtrip, safe) [ BRACK'NOT'OK(roundtrip,SIMPLEBRACK(safe)) ]; # ------------------------------------------------------------ # Bracket shortest unsafe string, ignoring word boundaries. define CLOSEBRACK1(notsafe) notsafe @> ... "]]"; define OPENBRACK1(notsafe) [ "]]" notsafe.r ] @> ... "[["; define ADDBRACK1(notsafe) [ NO'BRACKETS .o. CLOSEBRACK1(notsafe) .o. OPENBRACK1(notsafe).r ]; define SHORTUNSAFE'EX(notsafe) ADDBRACK1(notsafe) .o. EXPANDREGIONS; define SHORTUNSAFE'EX'NOT'OK(roundtrip, notsafe) [ BRACK'NOT'OK(roundtrip, SHORTUNSAFE'EX(notsafe)) ]; define SHORTUNSAFE(roundtrip) SHORTUNSAFE'EX(NOTSAFE(roundtrip)); define SHORTUNSAFE'NOT'OK(roundtrip) [ BRACK'NOT'OK(roundtrip, SHORTUNSAFE(roundtrip)) ]; # Bracket shortest unsafe string, but try to expand unsafe string to word # boundaries. define CLOSEBRACK2(notsafe,br) notsafe @> ... "]]" || _ [ br | .#. ]; define OPENBRACK2(notsafe,br) [ "]]" notsafe.r ] @> ... "[[" || _ [ br | .#. ]; define SHORTUNSAFEWORD'EX(notsafe,br) [ NO'BRACKETS .o. CLOSEBRACK2(notsafe,br) .o. OPENBRACK2(notsafe,br).r ] .o. EXPANDREGIONSWORD(br+); define SHORTUNSAFEWORD'EX'NOT'OK(roundtrip, notsafe, br) [ BRACK'NOT'OK(roundtrip, SHORTUNSAFEWORD'EX(notsafe, br)) ]; define SHORTUNSAFEWORD(roundtrip, br) [ SHORTUNSAFEWORD'EX(NOTSAFE(roundtrip), br) ]; define SHORTUNSAFEWORD'NOT'OK(roundtrip, br) [ BRACK'NOT'OK(roundtrip, SHORTUNSAFEWORD(roundtrip, br)) ]; # -------------------------------------------------------------------- # Alternate: bracket longest safe strings, then swap brackets. # (Longest-leftmost matches like used for LONGSAFE* can require larger state # machines than shortest-leftmost matches like SHORTUNSAFE* uses.) define ADDBRACK3(safe) safe @-> "[[" ... "]]"; define LONGSAFE'EX(safe) NO'BRACKETS .o. ADDBRACK3(safe) .o. SWAPBRACKETS; define LONGSAFE'EX'NOT'OK(roundtrip, safe) [ BRACK'NOT'OK(roundtrip, LONGSAFE'EX(safe)) ]; define LONGSAFE(roundtrip) LONGSAFE'EX(SAFE(roundtrip)); define LONGSAFE'NOT'OK(roundtrip) [ BRACK'NOT'OK(roundtrip, LONGSAFE(roundtrip)) ]; # Same, but respects word boundaries. `br` is a set of non-word characters. # Uses an extra "<<" token (as defined in brackets.foma) as a string boundary. define STRB "<<"; define LONGSAFEWORD'EX(safe, br) NO'BRACKETS .o. [ [..] -> STRB || .#. _ , _ .#. ] .o. [ [ [ [br|STRB] (?* [br|STRB]) ] .o. [ STRB -> 0 ] .o. safe].u @-> "[[" ... "]]" ] .o. SWAPBRACKETS .o. [ STRB -> 0 ] .o. EXPANDREGIONSWORD(br+); define LONGSAFEWORD'EX'NOT'OK(roundtrip, safe, br) [ BRACK'NOT'OK(roundtrip, LONGSAFEWORD'EX(safe, br)) ]; define LONGSAFEWORD(roundtrip, br) LONGSAFEWORD'EX(SAFE(roundtrip), br); define LONGSAFEWORD'NOT'OK(roundtrip, br) [ LONGSAFEWORD'EX'NOT'OK(roundtrip, SAFE(roundtrip), br) ];