@@ -825,3 +825,183 @@ describe('Iverson Bracket', () => {
825825 ` ) ;
826826 } ) ;
827827} ) ;
828+
829+ describe ( 'Predicate' , ( ) => {
830+ // Serialization tests
831+ it ( 'should serialize Predicate with one argument' , ( ) => {
832+ expect ( ce . box ( [ 'Predicate' , 'P' , 'x' ] ) . latex ) . toBe ( 'P(x)' ) ;
833+ } ) ;
834+
835+ it ( 'should serialize Predicate with multiple arguments' , ( ) => {
836+ expect ( ce . box ( [ 'Predicate' , 'Q' , 'a' , 'b' ] ) . latex ) . toBe ( 'Q(a, b)' ) ;
837+ expect ( ce . box ( [ 'Predicate' , 'R' , 'x' , 'y' , 'z' ] ) . latex ) . toBe ( 'R(x, y, z)' ) ;
838+ } ) ;
839+
840+ // Round-trip tests: parse -> serialize -> parse should give same result
841+ it ( 'should round-trip predicates inside ForAll' , ( ) => {
842+ const expr1 = ce . parse ( '\\forall x, P(x)' ) ;
843+ // Verify it contains Predicate
844+ expect ( expr1 . json ) . toMatchInlineSnapshot ( `
845+ [
846+ ForAll,
847+ x,
848+ [
849+ Predicate,
850+ P,
851+ x,
852+ ],
853+ ]
854+ ` ) ;
855+ // Serialize and re-parse
856+ const latex = expr1 . latex ;
857+ const expr2 = ce . parse ( latex ) ;
858+ expect ( expr2 . json ) . toEqual ( expr1 . json ) ;
859+ } ) ;
860+
861+ it ( 'should round-trip predicates inside Exists' , ( ) => {
862+ const expr1 = ce . parse ( '\\exists x, Q(x, y)' ) ;
863+ expect ( expr1 . json ) . toMatchInlineSnapshot ( `
864+ [
865+ Exists,
866+ x,
867+ [
868+ Predicate,
869+ Q,
870+ x,
871+ y,
872+ ],
873+ ]
874+ ` ) ;
875+ const expr2 = ce . parse ( expr1 . latex ) ;
876+ expect ( expr2 . json ) . toEqual ( expr1 . json ) ;
877+ } ) ;
878+
879+ it ( 'should round-trip nested quantifiers with predicates' , ( ) => {
880+ const expr1 = ce . parse ( '\\forall x, \\exists y, R(x, y)' ) ;
881+ expect ( expr1 . json ) . toMatchInlineSnapshot ( `
882+ [
883+ ForAll,
884+ x,
885+ [
886+ Exists,
887+ y,
888+ [
889+ Predicate,
890+ R,
891+ x,
892+ y,
893+ ],
894+ ],
895+ ]
896+ ` ) ;
897+ const expr2 = ce . parse ( expr1 . latex ) ;
898+ expect ( expr2 . json ) . toEqual ( expr1 . json ) ;
899+ } ) ;
900+
901+ // Type inference tests
902+ it ( 'should infer boolean type for Predicate' , ( ) => {
903+ const pred = ce . box ( [ 'Predicate' , 'P' , 'x' ] ) ;
904+ expect ( pred . type . toString ( ) ) . toBe ( 'boolean' ) ;
905+ } ) ;
906+
907+ it ( 'should allow Predicate in boolean contexts' , ( ) => {
908+ // Predicate should work as argument to And, Or, Not, etc.
909+ const expr1 = ce . box ( [ 'And' , [ 'Predicate' , 'P' , 'x' ] , [ 'Predicate' , 'Q' , 'x' ] ] ) ;
910+ expect ( expr1 . type . toString ( ) ) . toBe ( 'boolean' ) ;
911+
912+ const expr2 = ce . box ( [ 'Not' , [ 'Predicate' , 'P' , 'x' ] ] ) ;
913+ expect ( expr2 . type . toString ( ) ) . toBe ( 'boolean' ) ;
914+
915+ const expr3 = ce . box ( [ 'Implies' , [ 'Predicate' , 'P' , 'x' ] , [ 'Predicate' , 'Q' , 'x' ] ] ) ;
916+ expect ( expr3 . type . toString ( ) ) . toBe ( 'boolean' ) ;
917+ } ) ;
918+
919+ // D(f, x) should parse as Predicate, not derivative
920+ it ( 'should parse D(f, x) as Predicate, not derivative' , ( ) => {
921+ // Outside quantifier scope - D is special-cased to always be Predicate
922+ expect ( ce . parse ( 'D(f, x)' ) . json ) . toMatchInlineSnapshot ( `
923+ [
924+ Predicate,
925+ D,
926+ f,
927+ x,
928+ ]
929+ ` ) ;
930+
931+ // Inside quantifier scope
932+ expect ( ce . parse ( '\\forall x, D(x)' ) . json ) . toMatchInlineSnapshot ( `
933+ [
934+ ForAll,
935+ x,
936+ [
937+ Predicate,
938+ D,
939+ x,
940+ ],
941+ ]
942+ ` ) ;
943+ } ) ;
944+
945+ // Predicates outside quantifier scope should be regular function applications
946+ it ( 'should parse predicates outside quantifier scope as function applications' , ( ) => {
947+ // P(x) outside quantifier scope is a regular function application
948+ expect ( ce . parse ( 'P(x)' ) . json ) . toMatchInlineSnapshot ( `
949+ [
950+ P,
951+ x,
952+ ]
953+ ` ) ;
954+ expect ( ce . parse ( 'Q(a, b)' ) . json ) . toMatchInlineSnapshot ( `
955+ [
956+ Q,
957+ a,
958+ b,
959+ ]
960+ ` ) ;
961+ } ) ;
962+ } ) ;
963+
964+ describe ( 'Single-letter library functions' , ( ) => {
965+ // N is a library function for numeric evaluation, but N(x) in LaTeX
966+ // is not standard math notation. Like D, N is excluded from automatic
967+ // function recognition so it can be used as a variable.
968+ it ( 'should parse N(x) as Predicate, not as numeric function' , ( ) => {
969+ // N(x) should NOT be parsed as the numeric evaluation function
970+ // Instead, it's parsed as a Predicate (like D)
971+ const expr = ce . parse ( 'N(\\pi)' ) ;
972+ expect ( expr . json ) . toMatchInlineSnapshot ( `
973+ [
974+ Predicate,
975+ N,
976+ Pi,
977+ ]
978+ ` ) ;
979+ } ) ;
980+
981+ it ( 'should allow N function via MathJSON' , ( ) => {
982+ // N function can be constructed directly in MathJSON
983+ const expr = ce . box ( [ 'N' , 'Pi' ] ) ;
984+ expect ( expr . operator ) . toBe ( 'N' ) ;
985+ expect ( expr . op1 ?. symbol ) . toBe ( 'Pi' ) ;
986+
987+ // Direct .N() on Pi gives numeric value (preferred way to get numeric values)
988+ const piNumeric = ce . box ( 'Pi' ) . N ( ) ;
989+ expect ( piNumeric . numericValue ) . not . toBeNull ( ) ;
990+ } ) ;
991+
992+ // e and i are constants, not functions
993+ it ( 'should parse e as Euler constant' , ( ) => {
994+ expect ( ce . parse ( 'e' ) . json ) . toMatchInlineSnapshot ( `ExponentialE` ) ;
995+ } ) ;
996+
997+ it ( 'should parse i as imaginary unit' , ( ) => {
998+ // i is canonicalized to Complex representation
999+ expect ( ce . parse ( 'i' ) . json ) . toMatchInlineSnapshot ( `
1000+ [
1001+ Complex,
1002+ 0,
1003+ 1,
1004+ ]
1005+ ` ) ;
1006+ } ) ;
1007+ } ) ;
0 commit comments