From 0dcc9589e753d08963297073dd012c16e832b0d8 Mon Sep 17 00:00:00 2001 From: Manfred Bergmann Date: Wed, 25 Feb 2026 20:25:13 +0100 Subject: [PATCH 1/3] da and hm are now emmitting type tag even when used with alloc (internally). --- submods/dynarray/dynarray.b | 1 + submods/dynarray/dynarray.o | Bin 26284 -> 24340 bytes submods/hashmap/hashmap.b | 1 + submods/hashmap/hashmap.o | Bin 20564 -> 18860 bytes 4 files changed, 2 insertions(+) diff --git a/submods/dynarray/dynarray.b b/submods/dynarray/dynarray.b index bb47032..f0cba97 100644 --- a/submods/dynarray/dynarray.b +++ b/submods/dynarray/dynarray.b @@ -604,6 +604,7 @@ SUB DaNew(LONGINT initCap&) EXTERNAL DECLARE CLASS DynArray bld _daBldPtr = ALLOC(_DA_STRUCT_SIZE) + POKEL _daBldPtr, PEEKL(bld) ' stamp class descriptor for TYPECASE bld = _daBldPtr DaMake(bld, initCap&) END SUB diff --git a/submods/dynarray/dynarray.o b/submods/dynarray/dynarray.o index 87ef6e5caac63b96d2d28ebc1bac38c206fa2556..339b43e5ed6ec16952543f344cc1614709202b3f 100644 GIT binary patch literal 24340 zcmc&*4@{KTmcPKLBaSf2h%6; zm#*;{W4euL`pk>zYZ}{_#x$-mjoTP&8e^<+U1Eq~eXPrKSvpH5?$o7z`#a~}@80hl zM#Q$snw*ch=bn4+Ilpu6x%c~{5NZD*gh<=@=I)N3o{l%m_n{@y260K*v1ex|e*Z;? zY5&qLQtvlywl0>wN&QWq3sL)-6$iyYQP2=*5HtYl2lat^!`;^CqF039idK|a(Vostf`itXcXp>96yZs`Qro?it1Jt%9k3-*AVH%lLa_;dFP}BAsM{K{ZsOW+UB`9Mx{7O`bp_V} z>k_WL)k%a`=-KMzLG{q+R(oK-8WL}_`3f6 z_Y;}%-+vE{S1T$H2sr~BsLus}I+>BTtZ#38K!|HN}FEBacK`Awr>JAMTse8`$vJ83 zh7bL^{@LTkv7Ynu#^$W0s8hazt6hiqpX=?Zx}s)4XTQ-|+igw`JbuvhVf>&K>k!t# zXIEpVgeSE%m2^s|*=5h^tpvASh_hr9DzR5rVxKhxeY*Zqe;BqueuX(A8P`}0a0Wba ztOLgfdTc-AkFj=*xsEdd*+DhN#zW9)>aOt_)ngcRANdsTQD>rVcv~@vI12fI6ZuHN zp?B6FQYWTIz~Z>ZVp#q1S@(g(uyqgD`_|p@^UZ-nO#5|9YyV_NpaSPh z%TleNt@&}MOdUBoKRouVsio*-5x38|i6>4i<&!&&xB*;7O%9Mv$Qh3vf1T}qyT|rZ zwY`s(^d?C99xLgyB{f1G{mZL=`Sh>HBFR3LxINON0&di$|0%27Yk;s@iyhWOduyEc z*0{6|)#3qG#|W(wt`_$VYN!_XsapJRBdWy%WZ{FP)#3s2=nCP+=e7~`%g6dgc!dmO zg`j`@YB6?Sl-Wn@9CZKi5l%1Qv0%d^{5#0T}6@#QU~mY4GN4(4A;07efBMp z;_L%LF)M~^)QUb&^5z#Hvi1S?tA}eZa=lJp^p?O4p7o&JLp>b15nLZw!-yR2Ma@XW zY=k2>%#jge$cYQ?48R(BX!IrOz_?OG?IG63!!he)GFjKYORovC3D!J$HQ2ce zoF{3VhbOZCrG5(QPa7*?xjtpXlY7dR@lMfqVdao@hp%*^9<*+azr*p_>lUBA?g58; zHV!Y_d(cbJ60?5FiTv(&-z-{dT{He5ayK|~*Q_XNQxNx*XKBuAWTZUH=vkY1)^EqR zolm{D`P6&aHOsd=%VnBnbQxx8QnREbo#pEZ%yNfk`N}oR*F4KtJj)$a(m^$gCt;Qr z_T~IUW(ni&9vOMgHP3mR_qwff)(E`0fO!t7c_t;yLubyIb=EcZEbRiLL+$Ii@61GO zU{4)ch^U=mPfwGl4?j{X?IYxB)XarkjqyJJbmz2m+S|nXa6voWyUBfsDs~KKSL_~- zKcSA@b1**o-8W&cvhR1lh<%>DqJ4$3%xg`^GS12=&^lW3Mo1|4f@!xGe(S zqwE{@NizrHu6M`|@#qWX&3o>#KQZo;`H7~DtZ!n-a;=J1YGhM_uw}quahVmPeDcy+ zDaL1xm^v$|O8OHrl%BoPox~?PKkesR_FWY{+ZwV4o;F|g8A;t)u)Tx&j5Kcc+k1@T zd^F&=yP$j3dj=PdZxq)!-v`F5K|T#RBoON;ukj%~m8M-|2o>j^b-zQ5>{fB7dou^t z_TKa}5HFd63#v&eDh@DVn#}x?@hJ?9Y@{zOQX8u&cUn+_my;4!+0g{^K)O=Jub;T zTdP}w(83*SP(E+g_qRctPk-^mNIkcp=NbHtM^?ntajgxi=+ZecW;{l8`DqBx>e_#k z&ncf$>1QQ!5B@dw;6{??d%-wYix{*2PVHInNz!WH{cOU{C&7<-_~Gs5debZ7+NiN16BMn7u#Op8h+$ zvlNG6o8#_{?(=*eD_-*h{(8d~aQYLpXZyUgXY+f%Joh_IUP1Bj9}x9;1wHkNh5g~S z@F8k1YOzlajFDp=XHD)9L&q7Z18!STc}98eLd-qN`|qecFF39A7aA-5BuT4c!ZQz7 z^UN=~XLjjd?a;saY4v}f@kIUG9QwCCt^VN$Pti_+j z`j12Z1K|Gcj_LdNSpQmDAxi5gYdz!8dInm#fqyfjWe*F&C`S#h2d&uHCIr=5=S&3~2V<9xH9<$Wff{KGR@ zf9bRqfsV5Xoh(57oI+VY_Pv8TA(Jf0jOz`57a)$y_r2mU+41<@hbztiz3IU- zgcNf>Trp|n#PT-FGrqVNt_D0-sJ0bzFrNlZ#vri zy~t>-9&K$>AK~dLNLAB~@k7txTp&m_&dOr}1!iPQ}7=3)5;Pu|8 zP^U5d7(;jL;QfR3B}y_fu2XUJ0D=|EZZt#@8rchsKH9kcu1L6mcA5DsFLC+~|2_6md(~Hy-ureM6+Y zi)i$ktN^lORuSTjr!+byKYuNGRZRG8J9ggj*U}L19VJgDww6LduBDJ?{4t4ZDda^= zTx%)hr8tB03pvVQMYWw{f&0?&Aw5tShc&^z_ql zr=a&J#MQ(GYfJA=9pZETfMNV58qkhZV7M2Xn!Rd=t;wOoi$!TyN*j+4oPrt3VjtSpFQxNd5L?l z%aOb~VPGdu?ZOOr%8uS>1e#s?sDB0e=>&>*HqX9=j|1p?gCR(&J)SgGOu|#F8*4AB zGZRLoP(EKXlDuzUBjlfuPrY)DhkTH|HbGgs7vygPc)l@vGVPS1fJ5fm*d2srg5%rA zxqH*plThCHHja@G7n=H0N&7}9%-1~5^&npZeD(9yr}934J*3v;8s|5j!SP?e%KL!I z`=B+*{5)hEt&1=+%B+JyAT(&7GWB`x8P5D~aUHTQ<9f}R*~owW77f3@rXA*AM_FnJ z&nb}=?c&E_=#A=J!#ex%3vgr}B1<{{#E*yM+7a~3#c}6AcS7>J!4Tb556b7?@*c$W z&7nTmH-!=8EuyNvZAFVt73!-$@)Mi^*S(BquhpLSn4XmO)V=aOO{2RiKz=k*X5SlV z%t%+hyH9_m?b&Oz32_4Y9Crzfb$so?N2BmZKIg#qA3#8VyIr`t@B*Fa=EFbXS^Ack zEWEX^iS$`-iB#dMEfLfCR|a1*`ReEE3|v#hOul9_H*}lp@3=uMp`wK_`UBY7iplTOsDuf$j^DzaI3d5C!R=*FYDA zC|eEMhOx&%sZjAY=mH35#MFFHBj_OJ_!x8_U)6x;X$2huoyB~ZchWT>Ci_4wpcg^s zK=*{05&%Jd${V0lpc|k*r0^XOPVrN7K!qU4PQ`rbkWDYflP%abJr7g_dI_`(?m&Mtx?+cN=8nhntHt1a;X8#s+7<3+V32Pg&A;`{I26_$z**TX%uw~8w z2y^GJ0@Z=wL;i=LQ=l`TQQ+hO`9a%2J3)Ivu%iI_3!tX}GGXYB0E5DrAk0zN0D{eh zzXKfs0pG$CAYfK_0dxs;RfzfMpAX)A@a7MJ9tlx|`HIp(n6s!9v;?#YR42p&_`0AA zv>$W|^d$&m7kmx60vZ58&w?0eREXl6LM#*@FK7h_zAtP6Z2@fq;kIfaa9s#H79JI1 z5&CEkU33z32?RZ*g&_D>x(ZYaS_^6dZ368DK}RWcltM=-bd-L8oPz&l+c7czrG5Vn zE^Rg2_thWh+)>-HuM^B2Ao!U20C_ZmyudtO%;SY_?@r{yR&>|l|BS#!Upa{W9z!lb z_w=)%e$Xg#VOWSv=*e6H+6@B6nTU-a<7X6uj)0(hCdSXi*qL{*E4aaq3a-Eos$8AuQ`W=$VI$zF+Q&y zgfV$Xkze4=ISx99e1dF#HE1^o{^VZ+jUb20L9L)8Aj}(v?P1t7_Yv|6eUaUuzXu`T z<{=jIFjwKHAdD@FfORH4>3(&;pnuPT{D=>1@Wc@x=k$|D>stcC-{~KL?jaU2VE+v0 zB!)sRD;@MHu!URzcF#%$9Rao;x#5%GZbgRTN6j4wog(HY?K9I&_~LTAH2DT%;srtPl-EGWvnkN&1%GM=2)?C5_jCcGzpaQ5OjCb3$|pviJr}mx9_++aNO^@m=sD=m_F5jCe#)W8nWH*tQ5Zlx7g7ZTt53?AS}k z9WetKehaMPz#$(5Tqoy%V3QB}rvsN+@F{yM=7#QY1LB^C*lq#A*0MvOBdoJ#P5rK( zPO3eVF$MIbo&q5+ru_~CJE!%%hlyw%H0jNSn3$qQgC$Eg!1 z5d2F4_BU*v5(cmABb)cKR6x9QxTLjh++5pMSF@q!*Yy~l{+xr?&{|KtX?z<+hh2x^~jx!LGUNJ^w%~wLlORE zmXGOc+T5u6jyv@Is(uq2g)uW9CGg28{LA_%flo%^U%=@f^srIHKM#2uvrR9w5I%ti z4jmiKoU?wLz*lo-pGe@VIkSh|e0$E>y>7k@M6krow?PPg=-{{3Z)n~^RwD0LfN$Hr zrg?J{;uZSbA-krgrDn~#O)rq_w+{ZgP4%s<_3#Y*dkOre`mJD#9B%@DUENliGiMw4 zb{t91mYPO8COHR_^eK#Ti<0yyoN_ND%%y~cx%qa?@Sk5@^V%^hI1RpSBNh|cSU`EDv1_WU-`d<@ zV;7Dj=~K3bo#WOtH*Km}*R)X$oEvwH(|vMalzVVVv9oZ-S(wn9XnHW$|2iYU$=RU%*o=Z{sGOtUNz~-`Mmk(oue%t}r7?m&6+9;OQFY;OQC%p6#Ej#*O6@g42(U{Jn%#~u1yH7_G zt{UgyyJ}nlU)Q(eyZh0UQG<9?~Wp=zi7OOgo*2I!LApKk#oSh}+fN6gZp8_F~!Y!JkQO z^;_5B=!AozQ3NrnS3GtJ%chVDZZDN>oZDo3Ik!`f+U;espYnp`S8&^7>K~@rxOQE` zrpEdP4xvb;y3MjHSvHOGp4(OIY}#(!W=A~V>NY!?wjb@uE@p+$}(Ql%v_8|`Zl|NG5eg3dqXL|n0-!v8*Put75ipr8sW%#7c&;=$8=lzO4!JL z_9^|WZnIB5s_(L&eVV3yUd%pCqj_b&wEKu|v)$AFXt$TJ-P1nNZMJ*bIo)Rard`!- zwr|=E-Ddly-9_8wr#$@lB3e$`-HNsY>$)0sAOhQ`FnQw` zCydQ9#>U^T+w8kv$6^`#KBLa=U%|G|d|dW3`;^oq0gZv%NEQZCuXw%{;36*}j>l(RN^og69Ir z__u=X%{pO^U%~cfg>{?l&7xTt&lPNMmcfhd&7xSl)}=Z8XT7h-vM+%`-DY31F6uV> zlGUr*>`NBq1<$>bvCbMqd(2#N(FMX!4ETvbUwXRR`Aa%@&DF0Ohh z58Ja28aZ+}`vcP^hiBuoN_SRS_P+wMG}=5o|SW@;SH#mqgvFAO;OSRb+mM6eu6a=b+wys`$VYG??&# zCwN!OSCh}dF?o+zsNxtR0jay1d=An2S8a-Oi0o8t@++kCysDba3Yom&Lrv%qm@dD} zjcn)>^eJ8J*Of$Bb#n`L=#r5opF^~kU3$$;Z>V3(@?j`vGTM$BR#)S=5k^-|s*$H0 z$$8PV362~SyTyb_PLt^;Oma4vHer%O7`bet`Q!~Y_eq}@6J9xQB$Jgl-8ntzQ?isJ zxe?PQymC&OHer&Z_lhb$Oyv-sa{LnZ>jv6ma4-)mbMuWn`JH>tw8`(>M$;z0b6+uS z@;i6GX_Ifc2XIL`pIpniXN^4hm$%il$-i8aw@b*s+(){f4^DaMrcM6k1#w9_pIpy* zCQeJqzr1&G851YDp!0r$@jco(`Mi9pgm?TS!?Iy z7+Lar4(WE)WcjdTj#-OK36J~|TwFTM1Cab8^hx{q5S9O4GFcgie8NG=5{~&}>M!Y) z80OzIvV>v5dR!81P&Kij!N^kn6nvUQR@JzIkBuzhTM$bkt7=q1)W{O{VTz3lFZtjl zY;avhF$s^DaTJpH3DYKj=H4)EvVSh+pw!QY z)45bbliDluVy?l94|9=SW<1#+*<{*ef5gDt#*&1bg4Pi$V@p52{_*9)G@C-b1Nkn%<8B%g{- znl||~f30bgPxH6wb`{xBr1P`7l6;&``Cx3~^IB2Z=CMtZ*9xATm(OrTdRvXt3<( zhf*canKt=aQj3dAuY71+R&3-5gOb-wn=mMO*R;v!lEbD=F)jJnvY%^0V}zZu5Dn^pREQtkhf4gg*RB8vzaCN6OBQ{T=vyAHTEidH&%u>J@bz8#?}` z^O^9Dj=dc_Ufus@xcl{e`@`Ei!_S2)$98re*wNX!b6>cwrfoyb);8Qlu@d!8dH3sG zUGj4u;(9?I`d$j3|LoYad*6Oa1pK4#*Iazsq42K=GzYFpD- zvvFfx{l+zIbuTp4w6@l~z@749eDCh;1}p79Vb(hQjZ&j+fA`+buFmdG`^zNaNqgVu z=wWL;ET?&7sV9L)R(gr&Y3tsz^9_1GRNGkB!Y|`}#7}M8(bchUAL;WECXPAC6JMc` zA^r62usw!!`&XMb$@}F#$vP?b{0B@wtuOy^)27_>pTWg3Cy$}E>fq71faLA(?B3h8 zXZOqOe$dV1Kn1V!SX+*R6ujx;ae#uoE*?#b|J&!{(X=Ai@8Xfk`2Tz^9xXZf90D;@ z_R&%i!694>kIIJ#MqNBU69i*!9yt<>yLseDaMaBsM?xUS9Q?c{L~%9q@-v%|jIC{7 z6*&^}x#h@_kl)QCM?wKNj~odFT|7PqgydrZL7b#u!69q=)fvJRA=p#KU+QxzDrS z{r7%+^3$2-lC1ar*4lfoz5e#v>)r2bR4V0Plu{}Ce{s00x3}vT8~DJ2AX*gG#O}+6&1z`o0C#ig+kjH+Qv3+NL8-wrHOODJe>TJs+bcieEQV$ z$(?B%H%v@a7Iv!SZ-U-QUhjZY#RiBrujPMJ!nRi7+#mE+5y$zFefxCZNfIXDYcz{M z`XQ}lDt-^@-|6^00DeQbU&p;4_p7)M;(i(TKHR^?eE|0_aleWC#fF1{l=wZW`DDRj zNly0DzpPLqO;T*_#As24iah;4ds1O3mGoTa+;hL|QAzFKHX1A3s9sjpgyE5wYE=9{w7U1V08^?dI=6@ArWB zyC0m!S9GWd`t<4FsrW6i0(v@rk7;@aeckf+Ek8#*H)CD(h1OMQO>qR)5F>3^|7N6_ zsAHrJg7Yx$CgK?Nj5LX8-2G_sqI-&HI<2GWHE{aQ(G-r|tBBp%#U->Ih>I`avyZRY z$srNfABwpC*+$&#W|SE03&mhxC%YfWXzS!GY*8ps5VZ_Ff?J^uZZMoy`R#PT)9^51n# z+@8*tv&z!K)k<@XD+K(f%ua^?#yU204k^N% z80AJ`*o&r1eH2^=QMv z>cUD&QUAOzMH<^!4XMjoTJ8yEf8d@l1pc8t!D=@}ZQ%KR%-U;!5i^s~p0)4W(fyLi zf;}uQd&!QVvzKSHG}!E0XBA|odw{#pXUtSc#xp%)6Sf%nOpEbZQQ;}scWe#$P^|X8 zXSI9!H?785?%{q1_q(`%E4*iBu~Dn@9lge;St&urfG0=n$Zu&a&lc_B2&FXfHh9sG zi#*Bhx(43e+VH=EY__|#Qza#H^fXm=Fn*O8<&RgF#1?_Sq<&!?Pup?#;f~k%lb}-7 zv}iEj^mV%K&$Ya6*Y>)9day$#ErU(RU3{^X^;NuGL+dTW^fH#w zcQkF{}siVS#P*0R*yx zO#UOdb1idCf2-TFrrKGj*w=MN6m9O9I$)F(DRO#WG2r#tOJIi1uI(K z`Rv0dY(D!;v&&6j9+oa|NC3acwEnzGUbdxWie%y z1uNdxGky`8@siB=1!o+mjuAa$B;ky^wT>62^PA`>Gtv@RQ@;<*dI8mW&&2O1MlkCJ zRN-&xS?462wLs5$KE&fZce?RcI@@&XxtYrZPkV6N75+Zvktx_UZR*xBZ-A97~&@|1VV^)PvV1(#t)Z%pq@>Z9*PZksnD`6ewn zSImqhmxvf<3|dJ){az%^4m{*M$8)p4k<6RI62n`pz$7l{J#S90e&|vU4^svmS09FX z{H@}O`y8Jjtft&GX6zj~cNk`#wH3~K$T{F?ga~EqrZV4;N)XX0*`8ZLJgM-rd!J-?V;nR5TltnWYH|5eua z>Q483KRB!Bd%x!Vu9jwHIU`Ho`_frjot|Xr5$scVYLX}SMD^uJsJ>)fN#FBHiq9E5 z9g!z0U1xe;gum~J@9&E5?}+aQ==*!soTZP_UM zyZ4da30~Cn(D$rxec|5X>MZY0NfT=mwfoH_NHXg35%PsO?TaSP7mt!8@tL61Ge5=- zyF-tYGYcK@=FJ(Tnavwja2gHOH0Qrp9sJs+QES24M&7IAJ|iupz8MYGH(z|OQYWr+ z{&w>7gu_3j4|17L=?Tv5AMW?68`gB+W3-v|)@k%)u9}sfq|zC6l}^Li|2}~ykXIqM z_uT*ZNu|;w6Jzy%#yvImIcj#rNKcoV7gFQ}t2qAK21L(T>^Hnt@T(2K{^CyH&47$= zcl$2&ue)`1_^YrS%x*Go2}X}gr7|8=UA}}FB~HQ&_GHA)Qi6Yb+3qvUU7n<&nKpSd zZTfCA4Y_l)sb<>a&9vt`&a`JnI-T%lI`Q3R`uCF1Ox|9nt@nVDlySzJ=?rGN4?jJS zNHOO-tS}y;TJbm_Qg8-^T5y=p%m1EkXKLeG)BMMU|2dEUIq(lMJtNVs5w{b7FU~l@ z@hQtu*1X|pcM2JA_6kTm9{UaRyWL?PhU0~8jMJfcz;J8=9>)k9!9?* z!yms*zKwg|mGFBDe6@4G8P#92 zyw=ug@=s6!25UAc?n#MHbfzlTnlfulSx0|I?d97g^+E8piF$WB3rOx8VD6(H;j@)OS$o);MrQhwE6R@Jfi|#$4vD-5970^UWV#`U^^&?$JPS&Kz!JN#W(^h$ZIbI>} z|L7l6L7BS6yu+MSLA`+Z4NEbjxgMr{hq?IW*v<*>tufYn=)g0F(c*tnD#~vVr_437S_ZgUr2kd)ACzC$ryD72v_Gg%wq7j0h}mwI^h}JU zzqPq}d;7tnjZczN!RvLoUX|hb$buj8((?RXkbpejR5Jyx)YS zUr>>j(`J=E>}zY)9vX&+E;~N(iaYt&->7<-&3KHp7`-!yJHMS9!2PD@C6fUY#H#%& zb2#)>=3eck=R7arZQYb!;@3aJVr$AUJk^tjc&X2Ni6i@EB(<1wqa6)toA-dQ6aAI8_2r>)1Yz~fVVMTrO)@H`&>_2dv{rS-(~p2ugu zE~~Z2*LWUBMw!y%^j5Gg<#cUYkIUDI;%8>Q0r@4B`cxyuhT49v>4g zkIHpeu0vCJd`Np7@0cY5MzzPsLLR4#h{aGvVt3Yu7ZdNSNf+SHn=Zl0D9 z-+q59xj_3h+w<#8R`QT|XFRaMqt6h+D>;`nimc?^$aEtoujJg+_?4WSN?(E(uS)zn zK80V$1Ha~~z^}QHz^}QfA-@jURd_x4Mq1a?8{j$b=?n66jWY^(ZtS=8_)m~yXM^0G zGLG?=LY}wPD54_52qM32T!|^kyaD_m+R};P`qw^JwRLy1%#k$9;Q<$|;#r(u%UCC{Kd9+3yV6 zf~OL^1?G&6KfyB@-z`gqF{6UY5>IT`0-a)oUL8z6<*@h_f0V9;Sk$ApocBb5P z@VGLI{u$mX8=5M6a&_fqdA+Yp+dE2qrjkWg)OCMv%*~rl{sd!UScRtQspNi|8gkeCN+S@GI(xySM(Z_u9Am_p@lzW!R=ki==FnHG$Jm2w0DoF#ppj6FEfVVOB z3?LaSZsS$}*a7$z;5=Xe-$s@JUIqY<+{Zkt0EYnJIrlvP#{HlKP!4zta0xI17{`Km z4)8kQ0^m#hqc_MlZ&azY)qrZqng;;C`MUs}P^AWdaf{{xHUYNcbPKX%ya@o`49vN> z7jP2KRbL~IfS&tnz!ks^rIumra*SPG2f+9|$d)$<2)X#h)v+1o~#>0p3(3&utCls z;%G4d^xP4|59rI<5j%STm~Z*#h@U6`y63~M`LItu_~d^IxC(&33ZO^9D@qk^0gNjZ z1FzU+z%b$s@)kkPqDsIUfb#&zUR(g^1iS|L27tLsY5?d{0>7>Vl%h{5WGRJBOTSQR z^Ul5A?%5{3todg zp+hVgfU&Xru;(YRUmXB8E-nGQ2>{QMyRaFc6tvQhVW%kUvUfF za2z&h0HCdIPY|D*o4UHYUu0n7Zyw+*;3jfn1o}e8bn1N`^Y>$1AI6=;xa97hpLZh( z^Y6c)FZ4-*u1OEj1f7z%0$}sxj{sMZJ7J?p5#TvMHvsg=x3DqfoP#;$!Z&kU0IvYf z04@M7Bafy6(B}uc0j~m}&ktbd)EEH$Q=v~PX!GEQv^@9-^UQx5{^s_uOQnz^qIF8 z^MSJH6Tmpcss_-O=(6M-YzO_aV9TZ8wG?tJeFwI?2U~%@9J1#XAf|g^qic{e1Mn6A ze2Sq@@m0j`4*{T+!VaZhVp{NCa}05t2Y_$NA#24Q#BMumbOJDj_^SilfNqhFoiNzX z4j(*%3kLBI`pt)2^M}!%(y^2OI^MRj?E>I|v^~>x?2t~%!MGf-i0y$bBDBS}rlzLt zZCkn>-<+4wzE^CDX%GB2nJ)>p|H#mEXV7YH8`{Bu0cf>pxTJJ!-`UX7SpQ7@kDI_? z{t1t^rM-!?^vfP?b8}NYbYE}>w1^&C-`3XD(%7+m2YeHoOyAZFU!9sv-?AO_Ym?~h zO}=L;w>*A7YT6+}Va(F=adZlWzwGWfI)%btPIj0s zLXrM8=*DI{o^7LjqUm1W?QYJfKPTyW&fKB6{(8=3&0)Hkb2;@jwgw+#h~=M!>Bb0o zTRnPv(=)BRC?(=vfo^2q+`6*`{>p#T>$|zWt$y>i9nZ4wS&zPLM^k%y6J`een>cz) z({7Me{zx2sTO(Ew_KiKD8$XiIuKH%w%?|*YLD$!NAh@&}QPS zy=hld`*sPO)#)C6OKW>m{pPKd@3DnGeOoguPO@5a4s@f-mTjU;nQ!m*rpBF{gP1Ju z_4+aUMOCE*Z z*{~}r6+0hCw>b`Uqqog*Uf)oTi=%6KicnM8IoV&!QXCD_jn9f-3Db?wihbXQavb^_ z8EuXO&Dt(L$0cafvaUpZZO7R>#tH;=8|5`TZCfy~^gXm0yM}U{M-SyVpYG>4(2XuO z$9a82InL{AbDT%BInEm&%5iaYKgT6^ba(BoLKL@Te;IQ$4H+l{J`G{ zfZ)}FD*90HbpiecZgV=Cc5g%Jgu>7<0&7}V`qoI_q|*kk4e$>P{)FJk4-8%xw0~^y z^@7j&5Vycjm}dLdZCiFUH*FCYs+2vZ?~~Fuh5ioo5M@(FEG~-7dD-HkXv!GyxgkA5 z6U=$v_7}xduLHNX+x`>oL~|%_*cL$p$f;NDcrnI2Uac-oE!%gZU_v#;T`u*8?JI`j zBdyj&3^NbUDjFBV%`3CG7-}9h4(k${K-vS_h_TWRp}&N9n6}^c7dy^B20U0_^l8%$J#kvM?I-q1Bi}#{u}Ru_i;GRt zzOuO3B#rit^MzqP|A_4?_Dbje74Q~&%;%aHoF`~_#`(zNqVxPOfk#}dn1`Ws(t~kg zi}^MN*9E!`+IGwRF+H@3gnBs1CKVv~%R z#l24SJbLfTcxd~JZ8F;v_cwcDW{d4F_Q||qaj{RP(@*S^`MPZv`((bI zn7`RuGq0KcJT=6*$EOw-{TG*5T=ZXDZgJ6nF{MhNuc@mRvj&iusuTMxJqO(8lFhAc z&o(tTJ=4TTN45Af+gEI}WVgjRi&|piu}*YfV%PL~k$ve|+h1(4pY;n;wYnR1E*Q}2%F1loWVR4Z+%jStEMBb$_+b;4h<)~mjk#Ff!78m)J za*y(CiG&w2kkfCy$eVr0j9)MEF1>AWk$35s#YNuiG>eP8*;&B79Ed&M9yVm}w|zyI zoW&LwU9#V{xag8izlv__MVIVL27gj)oy~POX|7<=zg>zRsoFe6tNl1V9 zfR%g4^`%nFwkGNu_+?q6>q~`~?{GL3US@MoRTcGJcE+|>QQu`BI-DXcqx~ky75HVj zvt1Q+UjAu{Pa=Y%Us%Vep&Yo4&dHH=@;^oi@F>e#v0^3(n z=R9ZoD(alqYulyZ$$P`$)H&}xhg0V~+8}-|ogDMVTwm&(@A0Bu`Kw$z^~&GmaO#!c z?r`dr&vg-+%RR{EzvlW%9kI@)~meX|DpimaEp$UIjKMR!LD*!1&bsX_Eq{^E%q3U>7bSJvd+RfGur$EbUcr zB2nMqp}XK1+H_yqvT%dLX|Dp>OSjV|1++P+{GlKUcUI zsikf!+HsjQSCDI0yzRzP?;@_TP)-gGJBo^2KiZ?{EG{9Q?f|6dBimPssN(EIeFHxf zFLr%t$Kq3o`UW;EKI!_>hQ)Ui^wl}BnDz~9Qb!w>&|mTL>Ks?%a;+3)C0y_EeRYm1 zdEfD+?MoQvA$tV}FC{lzKl)^)TMtsKt!#1awEN0Vi$6i#R(@siI@){Xev3=7w(>KF z({3xjz$F~-p~Ie)PM#-dkJ2nRo-wtm+2Pb{Rj0+JxGDvz?J33Fs^=Zf7+dvQhf|+b zeGaGmtK52%;&j!05EIH9#6@X_9WTXP>AMaW`A<2V@|V8uaLQkL#^JPo>927K&1Vm; z`}Pp8Boc zj06w#VkWzSMp735SbrE_X`dw)$u;aV42tEFyx z?3BYrc5CbEO3Jq8L)T7S9%KGW>DYGo_~!1Tz15Y}XHB=EHNJTGfTq#MA)VYo;ToG$ zsw=5ena%grmDK6+Rk&yw>95CsXmP2Z%IJUHP8}cLgo~3&>b0^gLlc<-+7{4)aZ_PG1Wx zkXdNF=y|DY`BsZdjaJTl7s^?|0c-gSt{OIgdvM@eY4c;X)Mz!&qhBDK)M1sM zJDj$y{L0~?>o>qde1k*dn!T zA4+Yt?uem@4+Gk#cAOkkt-IoI>bdTw!>MP@GK)(+RTFbK?OMx=J?Gr~Oq+fmS>s}~ zmh#pNxpB02&A7ukk1!AXA+=C#)U{KtTE>?bdpu(>lX+vMj;UoH^kvfT32HfUFh?D2 zR=eBb^hvFY0jWi5dtEzYqV^+)i|(&FoI2JzpYu}~RmXk&d1h_aEo`p@!zhHhCGZ&%gI|cO5-Se(AJ{$B&w%bG;^aNFVag z@Nn|Z*yP&TKVy%>8GHP8&Wva5Wt?$1V=v7=x~T8H9>SVOrs*vu`rE_$T^kcCv`|J=-wQ1UXx3Id;O?LZs1#^ubkWDric4c vk=%?hjf&)Eg=thIHyWZz9gv$BrcsaFn9yw8(tx>)AT<-Zvg#e}%tw#S%W`(x5=yCg*Q6UL)f;(Tj*j7Q;~P~3Rb_FvT6^uCd3 ziW}2wP79HrZX~{QTDXkqh-%*|wl)Y?%@f9SUA1sO{k?d`<;Ek?nCEh1dK9_g2ddba;f2#-(B5^0tHwq6U1Q2Cv+YqlAQn!-;oZ|t4!b{YpXJLn3P zpFbNezjn6mhi%Wtg!=}p7|~WR*7uDm{5t^}BP-S%6hR|l-UW=QHE|$cDxjaU2DNkNE!JYr15vI!ws&R&^_ z38Rf#s|l~fT`FhoSGZlzko9Dpr71m!7_bF|DqJl*Pyc(|d-?1U;R?nwV7GAj7sHG3 zMz_M=rp6lbRU^$=oxapw3;J<{#+A82BTFArte-w}z#Th`ImQ->g!JT6kvP2#505Di zkA(_9f#+i4%g;L~^EittdZf2Q@cNQa%x)FVIqZ?RbGDizOry$7cKsHad`D+8=ACWD zdi46S9%p$y&dR)_btnYZq%F^JKMES5IgmA_D)T+Ys3{8=BgpwtV;I*FV+hw-<1Vga zj#=ZzU97aYamSeI75{e+tB}_cWnh>*q1Fe-d}<+2VBR5nrX1kxG~-EY>pEh!q!FAs zvDnPjnVjR>tW~;>JU#r$$JO32Y!3BExymaaY@32cBJ@_Qf$n7aRioD|>1D&MUI<2$v4 zwa6nlW;J)m0wk3dH2oa^lICWbALE%3Q@YOaa-n6~ZuK;)O);``cy5;Z=VU3ek1GJ? zr401&TA{lDG6-0VG4|Fd;+2@c8qAHCtk%FcE2|_|xUo+@maB8&(SG+6Yu6Mm*&FE8 zu1TJ@)-a()jST_gBd&i5ht*UGsoD+Gs`%?wa|P$ZHtueUYI7VgJx`w@~%e` zma0=pwXig-YIjENUZt(5<-#ki>uzuQxpfy4uK%DhOr)h8C&sMqA}U8nzS_a9<@vW+ zS;rZ^OpcAciz*-W@sL&+JHHF~&9fMt1LkUac|6Y|T2wP<2i=Qk`In?c*}eE!L9&Fi zZW^(iHYO=5+>?E;&Y&~4GiVc^K^t)5oSFaJAv3AzIh99~bo%0x=S=KUce}Y>4kITX z>6{=m$TN>K+NfK8tHp}1$$RJ3R5@X4`sUR%Ps-ixrlx;hO@Gqtfq69pNt(fVHG`I> z%1e*B^Qt?ay7Q~MfVvB+yOen@eJ$urZ+kL9q#t7a&5+fc0W-*e8DpB{rYA~eZB|+E zFl81z{0dnxt+L=DWr4ySQg;ikN;7NTge5n0)=<8-q7DS+R9Unns3k}(39f|+>2E$A zpz4~Z1b(W4H|Y6ARtF}usso>T)p^m+BaiX4a= zRcxq2K0_|a+w}addVb6P{_BOs{2lsM{hfI6AHv`6zn8yfRxjr7$hYe6zvTWy_&e?I zzoxs^qMi0hpD}LU`FZ4V<8zGs!1xSr>SkK6@w3rW(3oDR9;@0yr$t>OKBwvu z+q~4?O+Q!O=bnr8vvvX0C6DU)nYjic72*wrZ-+Wpmyqo!9T|LuPd7En3oB0el|_vHhvr zO1W*Fv!COQPSX1w=}T8LdOXlMh#1d;*0-g>A0>h-G-TIYZ2<4BKxXW*nTCh>M<()lfjB=bLMW6lQ)e&-2W$mm@c&MK4i zWsQ)6U(0Kwjx@9-_#Reo+7~hPl60i3NygCx}O<1m7_eRWWdkdu2?fdc@HF*AZhkyBdkLh8@ zH*4kf3G>d+@7)vp?PrR={Y>z;pE2VGje`}!Uy4$F({^3N>K>jycNza(`Ar*NFNQGI zkhMD0*WRH;`rlb#jpN&&i_U%g29-xQ-(RaQ|NKq_^@`p`582;aD~*xLKN+J!MCp6= z0$4crZdFd5#9=ZTK%Wu(8(g>#+kZpaUwIAsr4j7pzMpQ7?(Tdpymbpns_7c!Yk;r* z(9><_+rJRmu^L@ps&R&m`mxK?zxc}o{x^s>a>Ua&+QY)rcAjFTs@X8UBsEjs&|hlA zgm@LXV>^4$o2Y2Wm~H$U{*8n_gjs@4%FzhDH{`efH7ULU%!b~Azh|rI?KIBLp`Oqg z;_(bXyF|h|FW8XB?Prw-Q!lDHZ5)5R zv@nkDD+}Rx_KL>wENOmi922iEjN{qSg>b}1HI5i*er+6odclUn{uKCx{A;7$z&to+ zC_nlF8b=>#(!10dql%s|i`k`Psqhnh8tL!?Rj|B~0v7w_=Qt1%^-{&fGqF=2!FR00G53CARqF4jGKQ4^f`Ri0XiT=A+Q(LgFXXI z;0wzI5c(Chf%b!Lf#9EFU@3l6h!W_RtQI2lDdoB}~NdmJ&tJS$^}6KwPYzyA(mbQ3g-m~2BFo&+5Pp*mm09*x#OQ9bW1>JzJJ)l=W@J|u?7o7xs1cE<`abJ7|{>EG-CqS^d;qhA5$ zD};XwZ^N$huw@4bxQk(XNepxvbOCe|cEA^_-hdrp5Ns+3U;Yy8_zkdkfZ)T*Q^4v5 z0cMpS@pShbJ3hE?02@070o4N|#!r7080$dq0UO3&@hima3@`$-|6RlfnDQ`o{!_ph z1HN^j2fzsZq91{Vf$??Bc@Xp>=3Neg&m*uqauIW{w&w2dgQ;A9L>J5AhxP-KC3^!h zS5NP;eWuPu(&>*s-~T*m2mw4U#NP$H?gkJ#x?#KfGteX0@+@-l00_7|BcN#^GIoGo z2Eon@=y@p~u*C~MW%*!hE^Nh^*{~z~P1t+~HUodoHW2)mdl@v2oLmb!41zuR7^?u| z7hwEC%oT*ZC=4GUKT0rOX%px);THw>Hzti@}v(t27Yc}z!({mkb8RepyAMw z=LT?rBkAV?zL(IJ*0YEHDp<9U{5;DWx3~26_aCL8;+F|IY+hafY5?s8AwJ750oN4t z-96AhFxW@+CIc}7)~q)`@Oc*e=nI0Hc-&pR#}Ar?3A(@;+zhvU@CxC?n%vpa(z3f_ zN56@a+Mh!EM*x=AvwKfdPwTE-jo)oS*JY2}WIH-rNS5`KO}0JS(g} zi^5aTkR&(p6fRGan|KO;;ESHL$-2JZLB1;bz$R;MVQj^NHre*} zJ#BDRNf%_6|F$=FG;ZJ7^?md&8L-KBcC~bNw(M>p{a2IZZ7tp8@e+z*x)}%3|8C3o ztyrw`B(*)$7*%brrD!vKQ&y0o&Gb*%%V@K3(|o4g=wqBwr0Xu zg&IM>G}IZ@rq+l}rq+l}rq&2#mQNgOBuTE;NRnKw5vSa;GY2)-KE7pV9_ol)ZrPbn zMbEvLtn!swBQ}{@BQ}{@BQ}{@Bam5m9c#oUcdU^lxmqKT z&$~vF+8k>nMVne9DcaN;L7Ro!u|{li#~Oj$%0IP6Y`Ts$k|bAaBuTE;h*NIGs&d9D zw{j;!d2eCIyjJd1c_6ni+SiD?r@wb^4I1(L6eM-9p&$XM0{B6!Q;Qq?u(wLSj#+^{ zP4e}U$DShFW`Pb4zDqM~4X(`??%= zBkQ`bcS?Sf-$Kp8rEu4>@vakAdo3I4Qg+m`p{^UMoeg#2 zd|{5y;9c0yC11zwX-yVi%YfZJ#WP@c$l~i5UmEq2c-a8=V~S_M?jKlu9piOhQ9R>y zUsF8eO~aW;WH|ipaA?{ErO)`&_k)*t1K;o1+18AW2%@yNl^)yS!8ymYlkM=dDV}kA zsD`i|b&T8dQ;T2EcBXeJp6&1`zpiIH(u1m~k;9cG?CM?2gX% zCT!~I2bCV%no+8Fwkbp9&U&^fgM7g@tY@1t1}(mx+cRSpzn*Q(c+cYN*`|z3if5ZM z1Bz!GGiDXfHfH96cg9MCN0T>0=`-F;nuYPzv)!5GBgwOUnG}1;Gycr;if6o;uUq^E z#+&($;u&A&6~#+@YAtM#_S{zO><^7^gTynU+9jR`ikJ8kif4Sw9s@7yJ92M(`>tI( zyUa2wyxC}T`o^KKEUd<2+p?at_>FAavJS>&7}* zXAyF#$2Km%uXwg`xw2^^`y`9Bq(0lcT*Z7N+w6N+wM*PFi{He!vkoYpac7-SJmb#7 zqk$ZM6XVW0uXx6n^|s;}U)B}HGoCCJ(@hfZP1Vl$vhFEf;v2R2%@UuocQfPjc~v{( z^HJQ`{>_ZfSEhKz=OZoI&iH&ZtK=Dsulll0E0s-K*rx0Z)y_6$s~W$BZORTLkF5`pE2&no zy<6Dc?AwZGyRz>pp6yzx{JVwi@~gb9tsz5Js(h%eAwyT5xBA!C5criBaB{{fb{t`BXqkoS(J4G3URgdBUFmGtCqBe0g?L=e*Wk$S?Eo zSwMizvFJ8`R`Y~C|4Ypi_5zx~)FSrzt*~BSCDk29ra{aaGmDKuAufyJ-IvB zgv*?<<+=#kaFESKIzGIK2d5yEb8R5If-{;YyMnYPlgBpmyh!8V4Zi4#?n_NYs^->i zAiIjR|2L3bMcV%x$gZNxxHxLJe3)dpiILzJAHl`s$);kxo_O;veq6P0B%6vUF&*Q| z2hw7_#(7gO>D2uwhQ;?ZPxch6y{vX4*;71@i(_2*P+QWb`;na`gxBFi^Ff!|Y#7J~ zC9llURWFH3UP7C+n-2pelsC3BPAkqo>ctU_bye$=pH_j){(M*{&DK2msZ{%M6UDxC zt!^i5rNf#h|CFk;M(rl@PwAVwo%~a(Yo>N+lS#@8Fd-%{% z_8YXP9ADOnGF?lzkp1N+^mt@{x%NLFJj%8Iw~&41Wb~Za%5_#gsK+APD{NTF_VNj< zy^hZ$A!S<~pGiW?bvw;f@e9q9t)W)UQ!GPgG*7mKUei3mthj(nTF=fSC${$=8?37# z`zj7vvgV^l`b-(=Ic(A|Z|oSzrtmgg9JN3`iO^rz2##|dpJBrH!8^v(FL}bM-qi6K zrt)wqU6~J+YVFnWi-*eTRJt;@m6Hkszm$laN}+4k^oWXk9iNM;C>F_h&9g;B4#eKch$SNIQ%8w*Hm4E9KST!BmbY3{k_L~aX${B?}h)ISwXF> z@m;q$iT)lxg^_1Eu$Dk2db!-Q2Q!dk?+-+SS;>ZRSPW67P#c zmi9;N9X~X&rJ2}H^bRsIdKO6QIo=l^keDI#n6mglUw>b`kH0#3rA+FeFQNDPaY^rq zAKia~-k0sb7l-DZ&D{{1Kigbnm5=m2N`H5>y-TWlAlKuNd{=ujedWN9{AL@6yx^yC zY%_dv8Go~ei;OSHIO(DRhl~?08g$4w*`lS8>3B6o zn>)HXA&Q1@arf-&?>%;mCW+EMZi`1P92o6NlEHx;Bs2RFkgg;hdKl?Cr*#NeH}STy z>(H_Liq;{Z&k}FfLC0rDwN5>5W5nBa(6Kk5btqr@h_~yYV_&n@q5L^SJbr1@{}1Xi BM}PnT literal 20564 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 H#KHdukWPO| From 047943968e6119ae905680de425d00e1da3bb7ce Mon Sep 17 00:00:00 2001 From: Manfred Bergmann Date: Wed, 25 Feb 2026 22:14:57 +0100 Subject: [PATCH 2/3] JSON submodule - parser, generator, pretty-printer, recursive free Complete JSON library for ACE BASIC using Hashmap (objects) and DynArray (arrays) as the intermediate representation. TYPECASE discriminates object vs array at each node. Phases implemented: - Phase 0: Descriptor stamping in HmNew/DaNew for TYPECASE compatibility - Phase 1: Parser for objects with scalar values (string, int, bool, null) - Phase 2: Parser for arrays, nesting, floats, depth tracking - Phase 3: String escapes, JsParseFile, error edge cases - Phase 4: Compact JSON generator (JsWrite, JsToStr$) - Phase 5: Pretty-printer (JsWriteFmt) with 2-space indentation + round-trip tests - Phase 6: Recursive JsFree, JsMakeObj/JsMakeArr convenience helpers Public API: JsParse, JsParseFile, JsError$, JsRootType, JsWrite, JsWriteFmt, JsToStr$, JsFree, JsMakeObj, JsMakeArr 320 test assertions across 7 test files, all passing. Co-Authored-By: Claude Opus 4.6 --- include/submods/json.h | 43 + specs/json-submod-state.txt | 84 ++ specs/json-submod.txt | 705 +++++++++++++ submods/json/json.b | 991 ++++++++++++++++++ submods/json/make | 2 + submods/json/test_free.b | 188 ++++ submods/json/test_gen.b | 290 +++++ submods/json/test_parse_arr.b | 535 ++++++++++ submods/json/test_parse_misc.b | 352 +++++++ submods/json/test_parse_obj.b | 236 +++++ submods/json/test_roundtrip.b | 350 +++++++ submods/json/test_typecase.b | 168 +++ .../scripts/otherthenamiga/call-on-ustartup | 16 + 13 files changed, 3960 insertions(+) create mode 100644 include/submods/json.h create mode 100644 specs/json-submod-state.txt create mode 100644 specs/json-submod.txt create mode 100644 submods/json/json.b create mode 100644 submods/json/make create mode 100644 submods/json/test_free.b create mode 100644 submods/json/test_gen.b create mode 100644 submods/json/test_parse_arr.b create mode 100644 submods/json/test_parse_misc.b create mode 100644 submods/json/test_parse_obj.b create mode 100644 submods/json/test_roundtrip.b create mode 100644 submods/json/test_typecase.b diff --git a/include/submods/json.h b/include/submods/json.h new file mode 100644 index 0000000..25a389f --- /dev/null +++ b/include/submods/json.h @@ -0,0 +1,43 @@ +#ifndef JSON_H +#define JSON_H + +#include +#include + +{* ============== Constants ============== *} + +CONST JS_SUCCESS = 0 +CONST JS_ERR_SYNTAX = -1 +CONST JS_ERR_DEPTH = -2 +CONST JS_ERR_OVERFLOW = -3 +CONST JS_ERR_IO = -4 + +CONST JsObject = 0 +CONST JsArray = 1 + +CONST JS_MAX_INPUT = 8192 +CONST JS_MAX_DEPTH = 10 + +{* ============== Parser ============== *} + +DECLARE SUB LONGINT JsParse(src$) EXTERNAL +DECLARE SUB LONGINT JsParseFile(SHORTINT ch%) EXTERNAL +DECLARE SUB STRING JsError$ EXTERNAL +DECLARE SUB SHORTINT JsRootType EXTERNAL + +{* ============== Generator ============== *} + +DECLARE SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL +DECLARE SUB JsWriteFmt(ADDRESS root&, SHORTINT ch%) EXTERNAL +DECLARE SUB STRING JsToStr$(ADDRESS root&) EXTERNAL + +{* ============== Cleanup ============== *} + +DECLARE SUB JsFree(ADDRESS root&) EXTERNAL + +{* ============== Convenience ============== *} + +DECLARE SUB JsMakeObj(Hashmap hm, LONGINT cap&) EXTERNAL +DECLARE SUB JsMakeArr(DynArray da, LONGINT cap&) EXTERNAL + +#endif diff --git a/specs/json-submod-state.txt b/specs/json-submod-state.txt new file mode 100644 index 0000000..a642721 --- /dev/null +++ b/specs/json-submod-state.txt @@ -0,0 +1,84 @@ +# JSON Submodule — Implementation State + +Branch: json-submod + +## Phase 0: Descriptor stamping prerequisite +Status: COMPLETE +- [x] Add POKEL descriptor stamp in HmNew (hashmap.b) +- [x] Add POKEL descriptor stamp in DaNew (dynarray.b) +- [x] Create json.h header +- [x] Create test_typecase.b (Phase 0 TYPECASE discrimination test) +- [x] Test on emulator: existing hashmap/dynarray tests still pass +- [x] Test on emulator: test_typecase passes (8/8) + +## Phase 1: Parser — objects with scalar values +Status: COMPLETE +- [x] Create json.b with parser (helpers, string, number, container) +- [x] Create make script +- [x] Create test_parse_obj.b +- [x] Test on emulator: json module compiles +- [x] Test on emulator: test_parse_obj passes (44/44) +- Note: HmPutBool normalizes to 0/1 (not -1/0) +- Note: json.b uses _JsParseContainer (single recursive SUB) to avoid mutual recursion +- Note: json.b defines its own constants (can't include json.h due to DECLARE SUB conflicts) + +## Phase 2: Parser — arrays, nesting, floats +Status: COMPLETE +- [x] Create test_parse_arr.b (18 tests: empty array, string/int/bool/null arrays, + mixed-type, nested obj/arr, deeply nested, floats, TYPECASE, depth limit, + whitespace, int/float discrimination) +- [x] Fix recursion bug: local STRING in SUBs is BSS-backed, clobbered by recursive calls. + Fix: module-level depth-indexed key stack _jsKeyStk$(depth%) + hm/da restore from retVal& +- [x] Test on emulator: test_parse_arr passes (96/96) +- [x] Regression: test_parse_obj still passes (44/44) + +## Phase 3: Parser — escapes, file input, error edge cases +Status: COMPLETE +- [x] Create test_parse_misc.b (17 tests: escaped quotes/backslash/nl/tab/cr/bs/ff/slash, + multiple escapes, empty string, escaped key, escaped in array, + JsParseFile simple/multiline/array, error EOF/literal/bracket/empty, + whitespace with newlines/tabs) +- [x] Implement JsParseFile in json.b (reads file via LINE INPUT, sets shared state, + calls _JsParseContainer directly — avoids 256-byte string param copy limit) +- [x] Test on emulator: json module compiles +- [x] Test on emulator: test_parse_misc passes (55/55) +- [x] Regression: test_parse_obj still passes (44/44) — verified in prior run +- [x] Regression: test_parse_arr still passes (96/96) — verified in prior run + +## Phase 4: Generator — compact output +Status: COMPLETE +- [x] Add _JsWriteStr (string escaping: quote, backslash, LF, CR, TAB, BS, FF) +- [x] Add _JsWriteNode (single recursive SUB, TYPECASE dispatch, save/restore pattern) +- [x] Add JsWrite (public entry point, delegates to _JsWriteNode) +- [x] Add JsToStr$ (writes to T:js_tmp via channel #9, reads back, 4000 char limit) +- [x] Create test_gen.b (23 tests: empty obj/arr, string/int/neg/bool/null values, + multi-value, arrays, nested obj/arr/deep, string escaping for quote/backslash/ + newline/tab, JsWrite to file, key escaping, empty string value) +- [x] Test on emulator: json module compiles +- [x] Test on emulator: test_gen passes (23/23) +- [x] Regression: test_parse_obj still passes (44/44) + +## Phase 5: Pretty printer + round-trip +Status: COMPLETE +- [x] Add _JsWriteNodeFmt (recursive formatted writer with indent tracking) +- [x] Add JsWriteFmt (public entry point) +- [x] Create test_roundtrip.b (17 test groups: fmt empty/single/array/nested/bool-null, + compact round-trip int/str/mixed/1key/escape/nested, fmt round-trip arr/obj, + type preservation, deep nesting) +- [x] Test on emulator: json module compiles +- [x] Test on emulator: test_roundtrip passes (64/64) +- [x] Regression: test_parse_obj still passes (44/44) +- [x] Regression: test_gen still passes (23/23) + +## Phase 6: JsFree + JsMakeObj/JsMakeArr helpers +Status: COMPLETE +- [x] Add JsFree (recursive tree free using TYPECASE + save/restore pattern) +- [x] Add JsMakeObj (convenience wrapper for HmMake) +- [x] Add JsMakeArr (convenience wrapper for DaMake) +- [x] Create test_free.b (13 test groups: null safety, simple/empty obj/arr, + nested obj/arr, deep nesting, array of arrays, mixed types, + JsMakeObj/JsMakeArr TYPECASE, full parse+use+free cycle) +- [x] Test on emulator: json module compiles +- [x] Test on emulator: test_free passes (30/30) +- [x] Regression: test_roundtrip still passes (64/64) +- [x] Regression: test_gen still passes (23/23) diff --git a/specs/json-submod.txt b/specs/json-submod.txt new file mode 100644 index 0000000..271b2c6 --- /dev/null +++ b/specs/json-submod.txt @@ -0,0 +1,705 @@ +# JSON Submodule — Implementation Plan + +## Context + +A JSON parser and generator for ACE BASIC, using the existing Hashmap +(for JSON objects) and DynArray (for JSON arrays) as the intermediate +representation. TYPECASE provides clean runtime discrimination between +the two when walking the tree. + +Dependencies: hashmap.o, dynarray.o, testkit.o (tests only) + + +## Data Model + +JSON maps naturally onto the existing type-tagged structures: + + JSON value ACE representation Storage + ────────────── ────────────────────── ───────────────── + { ... } Hashmap HmTypeRef / DaTypeRef + [ ... ] DynArray HmTypeRef / DaTypeRef + "string" string value HmTypeStr / DaTypeStr + 42 LONGINT HmTypeLng / DaTypeLng + 3.14 SINGLE (FFP) HmTypeSng / DaTypeSng + true / false boolean HmTypeBool / DaTypeBool + null null HmTypeNull / DaTypeNull + +Nested objects and arrays are stored as Ref entries (HmTypeRef / DaTypeRef) +pointing to child Hashmap or DynArray instances. TYPECASE on the ref +address determines which type the child is — no extra discriminator needed. + +### Example mapping + + {"name":"Alice","age":30,"tags":["dev","amiga"],"addr":{"city":"Berlin"}} + + Root Hashmap: + "name" → HmTypeStr "Alice" + "age" → HmTypeLng 30 + "tags" → HmTypeRef → DynArray [DaTypeStr "dev", DaTypeStr "amiga"] + "addr" → HmTypeRef → Hashmap {"city" → HmTypeStr "Berlin"} + + +## Prerequisite: CLASS Descriptor Stamping + +TYPECASE requires a valid class descriptor pointer at offset 0 of each +CLASS instance. DECLARE CLASS sets this for BSS-backed instances, but +ALLOC'd instances (from builders and from the JSON parser) get raw memory +with no descriptor. + +Fix: stamp the descriptor in HmNew and DaNew after ALLOC. + + SUB HmNew(LONGINT theCap&) EXTERNAL + SHARED _hmBldPtr + DECLARE CLASS Hashmap bld + + _hmBldPtr = ALLOC(_HM_STRUCT_SIZE) + POKEL _hmBldPtr, PEEKL(bld) ' <-- stamp descriptor + bld = _hmBldPtr + HmMake(bld, theCap&) + END SUB + +Same pattern for DaNew. This is a 1-line addition per builder and makes +all heap-allocated CLASS instances TYPECASE-compatible. The JSON parser +uses the same stamp pattern when it creates nodes directly. + +Branch for this fix: apply before or as part of json-submod branch. + + +## Files to Create + + submods/json/json.b Implementation (~400-600 lines est.) + include/submods/json.h Header: constants, DECLARE SUBs + submods/json/make AmigaDOS build script + submods/json/test_parse_obj.b Phase 1: parse objects + submods/json/test_parse_arr.b Phase 2: parse arrays + nesting + submods/json/test_parse_misc.b Phase 3: escapes, file input, errors + submods/json/test_gen.b Phase 4: generator + submods/json/test_roundtrip.b Phase 5: parse → generate → compare + submods/json/test_free.b Phase 6: recursive free + +## Files to Modify + + submods/hashmap/hashmap.b Add descriptor stamp in HmNew + submods/dynarray/dynarray.b Add descriptor stamp in DaNew + +## Files to Reference (read-only patterns) + + submods/hashmap/hashmap.b DIM...ADDRESS overlay, builder, SHARED state + submods/dynarray/dynarray.b Append, iteration, DaTypeRef + include/submods/hashmap.h Header structure + include/submods/dynarray.h Header structure + include/submods/testkit.h Test assertions + +## Branch + + json-submod + + +## Constants + + ' Error codes + CONST JS_SUCCESS = 0 + CONST JS_ERR_SYNTAX = -1 ' Unexpected character or token + CONST JS_ERR_DEPTH = -2 ' Nesting too deep + CONST JS_ERR_OVERFLOW = -3 ' Input buffer overflow + CONST JS_ERR_IO = -4 ' File read error + + ' Root type (returned by JsRootType) + CONST JsObject = 0 + CONST JsArray = 1 + + ' Parser limits + CONST JS_MAX_INPUT = 8192 ' Max input buffer size (bytes) + CONST JS_MAX_DEPTH = 10 ' Max nesting depth + + +## Public API + +### Parser + + ' Parse JSON from string. Returns ADDRESS of root (Hashmap or DynArray). + ' Returns 0 on error. Call JsError$() for message. + DECLARE SUB LONGINT JsParse(src$) EXTERNAL + + ' Parse JSON from open file channel. Reads entire file into buffer. + ' Returns ADDRESS of root, or 0 on error. + DECLARE SUB LONGINT JsParseFile(SHORTINT ch%) EXTERNAL + + ' Last parser error message (empty string if no error). + DECLARE SUB STRING JsError$ EXTERNAL + + ' Type of root returned by last successful parse. + ' JsObject (0) or JsArray (1). + DECLARE SUB SHORTINT JsRootType EXTERNAL + +### Generator + + ' Write compact JSON to open file channel. + ' root& is ADDRESS of Hashmap (object) or DynArray (array). + DECLARE SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL + + ' Write pretty-printed JSON with indentation to file channel. + DECLARE SUB JsWriteFmt(ADDRESS root&, SHORTINT ch%) EXTERNAL + + ' Generate compact JSON as string. For small documents only. + ' Returns "" on overflow (> ~4000 chars). + DECLARE SUB STRING JsToStr$(ADDRESS root&) EXTERNAL + +### Cleanup + + ' Recursively free entire JSON tree (all nested Hashmaps and DynArrays). + ' Uses TYPECASE to determine node types. Calls HmFree+FREE / DaFree+FREE. + DECLARE SUB JsFree(ADDRESS root&) EXTERNAL + +### Convenience — not strictly needed, but reduce boilerplate + + ' Initialize a DynArray as a JSON array (DaMake + stamp descriptor). + ' Caller can then DaAppend* to it and HmPutRef / DaAppendRef it. + DECLARE SUB JsMakeArr(DynArray da, LONGINT cap&) EXTERNAL + + ' Initialize a Hashmap as a JSON object (HmMake + stamp descriptor). + DECLARE SUB JsMakeObj(Hashmap hm, LONGINT cap&) EXTERNAL + + +## Parser Architecture + +Recursive descent parser operating on a string buffer via PEEK +for fast byte-level access. + +### Module-level shared state + + LONGINT _jsBase& ' SADD of input string (base for PEEK) + LONGINT _jsPos& ' Current byte offset (0-based) + LONGINT _jsLen& ' Input length + LONGINT _jsDepth% ' Current nesting depth + SHORTINT _jsRootType% ' JsObject or JsArray + DIM _jsErr$ AS STRING ' Last error message + + ' Descriptor pointers (cached once at module init) + LONGINT _jsHmDesc& ' PEEKL of a DECLARE CLASS Hashmap instance + LONGINT _jsDaDesc& ' PEEKL of a DECLARE CLASS DynArray instance + +### Internal helpers + + _JsCh% ' Return byte at current position (PEEK), or -1 at EOF + _JsAdvance ' Increment _jsPos& + _JsSkipWs ' Skip spaces, tabs, CR, LF + _JsExpect(SHORTINT ch%) ' Assert current byte matches, advance; set error if not + _JsSetError(msg$) ' Set _jsErr$ if not already set + +### Recursive descent + + JsParse(src$) + _jsBase& = SADD(src$), _jsPos& = 0, _jsLen& = LEN(src$) + _jsErr$ = "", _jsDepth% = 0 + _JsSkipWs + ch = _JsCh% + IF ch = 123 THEN ' { + result = _JsParseObject + _jsRootType% = JsObject + ELSEIF ch = 91 THEN ' [ + result = _JsParseArray + _jsRootType% = JsArray + ELSE + _JsSetError("Expected { or [") + result = 0 + END IF + JsParse = result + + _JsParseObject -> LONGINT (ADDRESS of new Hashmap) + _jsDepth% + 1, check JS_MAX_DEPTH + Allocate Hashmap (ALLOC + stamp descriptor + HmMake) + _JsAdvance (skip '{') + _JsSkipWs + IF _JsCh% <> 125 THEN ' not '}' + Loop: + key$ = _JsParseString$ + _JsSkipWs, _JsExpect(58) ' ':' + _JsSkipWs + _JsParseValue(hm, key$) + _JsSkipWs + IF _JsCh% = 44 THEN _JsAdvance ELSE exit loop ' ',' + _JsSkipWs + END IF + _JsExpect(125) ' '}' + _jsDepth% - 1 + return ADDRESS of hm + + _JsParseArray -> LONGINT (ADDRESS of new DynArray) + _jsDepth% + 1, check JS_MAX_DEPTH + Allocate DynArray (ALLOC + stamp descriptor + DaMake) + _JsAdvance (skip '[') + _JsSkipWs + IF _JsCh% <> 93 THEN ' not ']' + Loop: + _JsSkipWs + _JsParseValueArr(da) + _JsSkipWs + IF _JsCh% = 44 THEN _JsAdvance ELSE exit loop + _JsSkipWs + END IF + _JsExpect(93) ' ']' + _jsDepth% - 1 + return ADDRESS of da + + _JsParseValue(Hashmap parent, key$) + Peek at _JsCh%: + 34 (") → s$ = _JsParseString$ → HmPut$(parent, key$, s$) + 123 ({) → addr& = _JsParseObject → HmPutRef(parent, key$, addr&) + 91 ([) → addr& = _JsParseArray → HmPutRef(parent, key$, addr&) + 116 (t) → _JsExpectLiteral("true") → HmPutBool(parent, key$, -1) + 102 (f) → _JsExpectLiteral("false") → HmPutBool(parent, key$, 0) + 110 (n) → _JsExpectLiteral("null") → HmPutNull(parent, key$) + digit/- → _JsParseNumber(parent, key$) + + _JsParseValueArr(DynArray parent) + Same dispatch as above but uses DaAppend* instead of HmPut* + + _JsParseString$ -> STRING + _JsExpect(34) ' opening " + Build result string char by char: + 92 (\) → read next char for escape sequence + 34 (") → end of string + other → append to result + _JsAdvance past closing " + Return result + + _JsParseNumber(Hashmap parent, key$) + Scan digits, optional '.', optional 'e'/'E' into a buffer string + If buffer contains '.' or 'e'/'E': + val! = VAL(buffer$) + HmPut!(parent, key$, val!) + Else: + val& = VAL(buffer$) ' VAL returns SINGLE, CLNG for integer + HmPut&(parent, key$, val&) + + _JsParseNumberArr(DynArray parent) + Same logic but uses DaAppend& / DaAppend! + +### JsParseFile implementation + + Read file contents into an ALLOC'd buffer via repeated GET # or + a block read (_Read library call). Copy into module-level string. + Then call JsParse on the string. + + +## Generator Architecture + +Tree walker using TYPECASE to distinguish Hashmap (object) vs +DynArray (array) at each node. + +### Core dispatch + + SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL + TYPECASE root& + CASE Hashmap hm + _JsWriteObj(hm, ch%) + CASE DynArray da + _JsWriteArr(da, ch%) + END TYPECASE + END SUB + +### Object writer + + _JsWriteObj(Hashmap hm, SHORTINT ch%) + PRINT #ch%, "{"; + HmIterReset(hm) + SHORTINT first% = -1 + WHILE HmIterNext(hm) + IF NOT first% THEN PRINT #ch%, ","; + first% = 0 + _JsWriteStr(HmIterKey$(hm), ch%) + PRINT #ch%, ":"; + _JsWriteTypedVal(HmIterType(hm), HmIterVal$(hm), ~ + HmIterVal&(hm), ch%) + WEND + PRINT #ch%, "}"; + +### Array writer + + _JsWriteArr(DynArray da, SHORTINT ch%) + PRINT #ch%, "["; + LONGINT i& + FOR i& = 0 TO DaCount(da) - 1 + IF i& > 0 THEN PRINT #ch%, ","; + _JsWriteTypedVal(DaType(da, i&), DaGet$(da, i&), ~ + DaGet&(da, i&), ch%) + NEXT + PRINT #ch%, "]"; + +### Value writer (dispatches on type tag) + + _JsWriteTypedVal(SHORTINT typ%, val$, LONGINT valL&, SHORTINT ch%) + IF typ% = HmTypeStr THEN + _JsWriteStr(val$, ch%) + ELSEIF typ% = HmTypeLng THEN + PRINT #ch%, LTRIM$(STR$(valL&)); + ELSEIF typ% = HmTypeSng THEN + ' Convert FFP bits back to SINGLE, print + _JsWriteFloat(valL&, ch%) + ELSEIF typ% = HmTypeBool THEN + IF valL& THEN PRINT #ch%, "true"; ELSE PRINT #ch%, "false"; + ELSEIF typ% = HmTypeNull THEN + PRINT #ch%, "null"; + ELSEIF typ% = HmTypeRef THEN + ' Recurse — TYPECASE determines object vs array + JsWrite(valL&, ch%) + END IF + +### String writer (with escaping) + + _JsWriteStr(s$, ch%) + PRINT #ch%, CHR$(34); + FOR each byte in s$: + 34 (") → PRINT #ch%, "\"; CHR$(34); + 92 (\) → PRINT #ch%, "\\"; + 10 (LF) → PRINT #ch%, "\n"; + 13 (CR) → PRINT #ch%, "\r"; + 9 (TAB)→ PRINT #ch%, "\t"; + < 32 → skip or \uXXXX (v2) + other → PRINT #ch%, CHR$(byte); + NEXT + PRINT #ch%, CHR$(34); + +### Pretty printer + + JsWriteFmt uses the same structure as JsWrite but passes an indent + depth counter. Each nested object/array increases indent by 2 spaces. + Newlines after '{', '[', ',', before '}', ']'. + +### String generator (JsToStr$) + + Write to a temporary file (T:js_tmp), then read back as string. + Or: build via string concatenation with overflow check. + If result exceeds ~4000 chars, return "". + + +## Recursive Free (JsFree) + + SUB JsFree(ADDRESS root&) EXTERNAL + IF root& = 0 THEN EXIT SUB + + TYPECASE root& + CASE Hashmap hm + ' Walk all entries, recurse into Ref children + HmIterReset(hm) + WHILE HmIterNext(hm) + IF HmIterType(hm) = HmTypeRef THEN + JsFree(HmIterVal&(hm)) + END IF + WEND + HmFree(hm) + FREE root& + + CASE DynArray da + ' Walk all elements, recurse into Ref children + LONGINT i& + FOR i& = 0 TO DaCount(da) - 1 + IF DaType(da, i&) = DaTypeRef THEN + JsFree(DaGet&(da, i&)) + END IF + NEXT + DaFree(da) + FREE root& + END TYPECASE + END SUB + + +## String Escape Handling + +### Parse direction (JSON → BASIC string) + + \" → " (CHR$(34)) + \\ → \ (CHR$(92)) + \/ → / (CHR$(47)) + \n → LF (CHR$(10)) + \r → CR (CHR$(13)) + \t → TAB (CHR$(9)) + \b → BS (CHR$(8)) + \f → FF (CHR$(12)) + \uXXXX → skip for v1 (copy literally as \uXXXX) + +### Generate direction (BASIC string → JSON) + + " → \" + \ → \\ + LF → \n + CR → \r + TAB → \t + < 32 → skip (or \u00XX in v2) + + +## Number Handling + +### Parse + + Scan: optional '-', digits, optional '.'+digits, optional 'e'/'E'[+-]digits + Collect into a buffer string. + + Classification: + - Contains '.' or 'e'/'E' → SINGLE (FFP) via VAL() + - Otherwise → LONGINT via VAL() then CLNG() + - LONGINT range: -2,147,483,648 to 2,147,483,647 + - Values outside LONGINT range with no decimal → store as SINGLE + +### Generate + + - HmTypeLng → LTRIM$(STR$(val&)) (no leading space) + - HmTypeSng → LTRIM$(STR$(val!)) (ACE prints FFP with ~7 digits) + + Note: round-trip precision is limited to ~7 significant digits (FFP). + + +## Limitations + + - Input buffer: 8192 bytes max (JS_MAX_INPUT). Larger files truncated. + - String keys: 63 chars max (HM_KEY_SIZE - 1, hashmap limit) + - String values: 255 chars max (HM_VAL_SIZE - 1 / DA_VAL_SIZE - 1) + - Nesting depth: 10 levels max (JS_MAX_DEPTH, stack-limited) + - Array elements: up to 2048 (DA_MAX_CAP, with auto-growth) + - Object entries: up to ~358 (70% of HM_LARGE = 512) + - Float precision: ~7 significant digits (Amiga FFP) + - No \uXXXX Unicode escape support in v1 + - No streaming parse — entire input buffered first + - Duplicate JSON keys: last value wins (hashmap overwrites) + - JsToStr$ limited to ~4000 chars + - PRINT #n, "" writes a null byte — generator uses PRINT #n, CHR$(c); + for character-level output to avoid this + + +## Implementation Phases + +### Phase 0: Descriptor stamping prerequisite + + Files: hashmap.b, dynarray.b + Change: Add POKEL ..., PEEKL(bld) after ALLOC in HmNew and DaNew + Test: Existing hashmap/dynarray tests still pass + new TYPECASE test + +### Phase 1: Parser — objects with scalar values + + Parse: { "key": "str", "key": 42, "key": true, "key": null } + No nesting, no arrays, no floats yet. + + Internal subs: + _JsCh%, _JsAdvance, _JsSkipWs, _JsExpect + _JsSetError, JsError$, JsRootType + _JsParseString$, _JsParseObject + _JsParseValue (string, integer, bool, null paths) + JsParse + + Test: test_parse_obj.b + - Empty object {} + - Single string key-value + - Multiple key-value pairs + - Integer values (positive, negative, zero) + - Boolean true/false + - Null values + - Mixed types in one object + - Parser error cases (missing colon, missing quote, trailing comma) + +### Phase 2: Parser — arrays, nesting, floats + + Parse: [...], nested {}, nested [], float values + + Internal subs: + _JsParseArray, _JsParseValueArr + _JsParseNumber / _JsParseNumberArr (float path) + depth tracking and JS_MAX_DEPTH check + + Test: test_parse_arr.b + - Empty array [] + - Array of strings, integers, booleans, nulls + - Mixed-type array + - Nested object inside array + - Nested array inside object + - Nested array inside array + - Deeply nested structure + - Float values (positive, negative, with exponent) + - TYPECASE discrimination on nested refs + - Depth limit error + +### Phase 3: Parser — escapes, file input, error edge cases + + String escape handling in _JsParseString$ + JsParseFile implementation + Whitespace tolerance (tabs, newlines in input) + + Test: test_parse_misc.b + - Escaped quotes, backslashes, newlines, tabs in strings + - Whitespace-heavy input (newlines between tokens) + - JsParseFile from a written file + - Empty string values + - Error: unterminated string + - Error: unexpected EOF + - Error: invalid literal (e.g. "tru") + +### Phase 4: Generator — compact output + + JsWrite, JsToStr$ + _JsWriteObj, _JsWriteArr, _JsWriteTypedVal, _JsWriteStr + String escaping in output + + Test: test_gen.b + - Generate simple object + - Generate array + - Generate nested object + array + - Generate all value types + - String escaping in output (quotes, backslash, newline) + - JsToStr$ for small document + - JsToStr$ overflow returns "" + +### Phase 5: Pretty printer + round-trip + + JsWriteFmt with indentation + Round-trip: parse JSON → generate → compare output + + Test: test_roundtrip.b + - Parse then generate, compare strings + - Round-trip with nested structures + - Pretty-print output format verification + - Round-trip preserves types (int stays int, float stays float) + +### Phase 6: JsFree + JsMakeObj/JsMakeArr helpers + + Recursive free of entire tree + Convenience constructors with descriptor stamping + + Test: test_free.b + - Free simple object (no crash) + - Free nested object+array tree (no crash) + - Free empty object/array + - JsMakeObj / JsMakeArr produce TYPECASE-able instances + - Double-free safety (root& = 0 after free) + + +## Usage Examples + +### Parse and access + + REM #using ace:submods/hashmap/hashmap.o + REM #using ace:submods/dynarray/dynarray.o + REM #using ace:submods/json/json.o + #include + + DIM src$ AS STRING SIZE 4096 + src$ = "{" + CHR$(34) + "name" + CHR$(34) + ":" + CHR$(34) + "Alice" + CHR$(34) + "}" + + LONGINT root& + root& = JsParse(src$) + IF root& = 0 THEN + PRINT "Parse error: "; JsError$ + STOP + END IF + + DECLARE CLASS Hashmap doc + doc = root& + PRINT HmGet$(doc, "name") ' Alice + + JsFree(root&) + +### Parse with nested array + + src$ = ... JSON with "tags":["a","b"] ... + root& = JsParse(src$) + DECLARE CLASS Hashmap doc + doc = root& + + ' Access nested array via TYPECASE + LONGINT ref& + ref& = HmGetRef(doc, "tags") + + TYPECASE ref& + CASE DynArray tags + PRINT DaGet$(tags, 0) ' a + PRINT DaCount(tags) ' 2 + END TYPECASE + + JsFree(root&) + +### Build and generate + + DECLARE CLASS Hashmap resp + JsMakeObj(resp, HM_MEDIUM) + HmPut$(resp, "status", "ok") + HmPut&(resp, "code", 200) + + DECLARE CLASS DynArray items + JsMakeArr(items, DA_SMALL) + DaAppend$(items, "one") + DaAppend$(items, "two") + HmPutRef(resp, "items", items) + + OPEN "O", #1, "resp.json" + JsWrite(resp, 1) + CLOSE #1 + ' File: {"status":"ok","code":200,"items":["one","two"]} + + ' Manual cleanup (not JsFree — we own the structs on BSS) + DaFree(items) + HmFree(resp) + +### Round-trip + + root& = JsParse(original$) + OPEN "O", #1, "T:roundtrip.json" + JsWrite(root&, 1) + CLOSE #1 + JsFree(root&) + + +## Notes on PRINT # and Null Bytes + +ACE's PRINT #n, "" writes a null byte (0x00) before the newline. +The generator must avoid PRINT #n, "". Instead: +- Use PRINT #n, CHR$(c); for single characters +- Use PRINT #n, str$; (with semicolon, non-empty strings only) +- Newlines in pretty-print: PRINT #n, "" is acceptable there since + the null byte precedes a newline we want anyway — but test this. + Safer: use PRINT #n, CHR$(10); explicitly. + + +## Header Template (include/submods/json.h) + + #ifndef JSON_H + #define JSON_H + + #include + #include + + {* ============== Constants ============== *} + + CONST JS_SUCCESS = 0 + CONST JS_ERR_SYNTAX = -1 + CONST JS_ERR_DEPTH = -2 + CONST JS_ERR_OVERFLOW = -3 + CONST JS_ERR_IO = -4 + + CONST JsObject = 0 + CONST JsArray = 1 + + CONST JS_MAX_INPUT = 8192 + CONST JS_MAX_DEPTH = 10 + + {* ============== Parser ============== *} + + DECLARE SUB LONGINT JsParse(src$) EXTERNAL + DECLARE SUB LONGINT JsParseFile(SHORTINT ch%) EXTERNAL + DECLARE SUB STRING JsError$ EXTERNAL + DECLARE SUB SHORTINT JsRootType EXTERNAL + + {* ============== Generator ============== *} + + DECLARE SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL + DECLARE SUB JsWriteFmt(ADDRESS root&, SHORTINT ch%) EXTERNAL + DECLARE SUB STRING JsToStr$(ADDRESS root&) EXTERNAL + + {* ============== Cleanup ============== *} + + DECLARE SUB JsFree(ADDRESS root&) EXTERNAL + + {* ============== Convenience ============== *} + + DECLARE SUB JsMakeObj(Hashmap hm, LONGINT cap&) EXTERNAL + DECLARE SUB JsMakeArr(DynArray da, LONGINT cap&) EXTERNAL + + #endif diff --git a/submods/json/json.b b/submods/json/json.b new file mode 100644 index 0000000..08941e7 --- /dev/null +++ b/submods/json/json.b @@ -0,0 +1,991 @@ +REM json.b - JSON parser and generator for ACE BASIC +REM +REM Dependencies: hashmap.o, dynarray.o +REM Uses Hashmap for JSON objects, DynArray for JSON arrays. +REM TYPECASE discriminates object vs array at each node. + +#include +#include + +{* ============== Constants ============== *} + +CONST _JS_HM_SIZE = 48 ' Hashmap CLASS struct size (4 + 6*4 + 5*4) +CONST _JS_DA_SIZE = 28 ' DynArray CLASS struct size (4 + 3*4 + 3*4) + +CONST JS_SUCCESS = 0 +CONST JS_ERR_SYNTAX = -1 +CONST JS_ERR_DEPTH = -2 +CONST JS_ERR_OVERFLOW = -3 +CONST JS_ERR_IO = -4 + +CONST JsObject = 0 +CONST JsArray = 1 + +CONST JS_MAX_INPUT = 8192 +CONST JS_MAX_DEPTH = 10 + +{* ============== Module-level shared state ============== *} + +LONGINT _jsBase& ' SADD of input string +LONGINT _jsPos& ' Current byte offset (0-based) +LONGINT _jsLen& ' Input length +SHORTINT _jsDepth% ' Current nesting depth +SHORTINT _jsRootType% ' JsObject(0) or JsArray(1) +STRING _jsErr$ SIZE 128 + +' Cached class descriptor pointers +LONGINT _jsHmDesc& +LONGINT _jsDaDesc& +SHORTINT _jsInited% + +' Number scanner results (shared between _JsScanNumber and callers) +STRING _jsNumBuf$ SIZE 32 +SHORTINT _jsNumIsFloat% +LONGINT _jsNumInt& + +' Depth-indexed key stack (avoids BSS clobbering in recursive calls) +DIM _jsKeyStk$(10) SIZE 64 + +{* ============== Internal: Descriptor cache ============== *} + +SUB _JsInitDesc + SHARED _jsHmDesc&, _jsDaDesc&, _jsInited% + IF _jsInited% THEN EXIT SUB + + DECLARE CLASS Hashmap tmpHm + DECLARE CLASS DynArray tmpDa + _jsHmDesc& = PEEKL(tmpHm) + _jsDaDesc& = PEEKL(tmpDa) + _jsInited% = -1 +END SUB + +{* ============== Internal: Scanner helpers ============== *} + +SUB SHORTINT _JsCh% + SHARED _jsBase&, _jsPos&, _jsLen& + SHORTINT retVal% + IF _jsPos& >= _jsLen& THEN + retVal% = -1 + ELSE + retVal% = PEEK(_jsBase& + _jsPos&) + END IF + _JsCh% = retVal% +END SUB + +SUB _JsSkipWs + SHARED _jsBase&, _jsPos&, _jsLen& + SHORTINT ch% + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WHILE ch% = 32 OR ch% = 9 OR ch% = 10 OR ch% = 13 + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WEND +END SUB + +SUB _JsExpect(SHORTINT expected%) + SHARED _jsBase&, _jsPos&, _jsLen&, _jsErr$ + IF LEN(_jsErr$) > 0 THEN EXIT SUB + SHORTINT ch% + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + IF ch% = expected% THEN + _jsPos& = _jsPos& + 1 + ELSE + IF LEN(_jsErr$) = 0 THEN + _jsErr$ = "Expected " + CHR$(expected%) + END IF + END IF +END SUB + +SUB _JsSetError(msg$) + SHARED _jsErr$ + IF LEN(_jsErr$) = 0 THEN + _jsErr$ = msg$ + END IF +END SUB + +{* ============== Internal: Literal matching ============== *} + +SUB SHORTINT _JsMatchLit(lit$) + SHARED _jsBase&, _jsPos&, _jsLen& + LONGINT litLen&, i& + SHORTINT retVal% + retVal% = 0 + litLen& = LEN(lit$) + + IF _jsPos& + litLen& <= _jsLen& THEN + retVal% = -1 + i& = 0 + WHILE i& < litLen& AND retVal% + IF PEEK(_jsBase& + _jsPos& + i&) <> ASC(MID$(lit$, i& + 1, 1)) THEN + retVal% = 0 + END IF + i& = i& + 1 + WEND + IF retVal% THEN + _jsPos& = _jsPos& + litLen& + END IF + END IF + + _JsMatchLit = retVal% +END SUB + +{* ============== Internal: String parsing ============== *} + +SUB STRING _JsParseString$ + SHARED _jsBase&, _jsPos&, _jsLen&, _jsErr$ + STRING retVal$ SIZE 256 + SHORTINT ch% + retVal$ = "" + + IF LEN(_jsErr$) > 0 THEN + _JsParseString$ = retVal$ + EXIT SUB + END IF + + ' Expect opening quote + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% <> 34 THEN + _JsSetError("Expected string") + _JsParseString$ = retVal$ + EXIT SUB + END IF + + _jsPos& = _jsPos& + 1 + + ' Scan string content + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + WHILE ch% <> 34 AND ch% >= 0 + IF ch% = 92 THEN + ' Backslash escape + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + IF ch% = 34 THEN + retVal$ = retVal$ + CHR$(34) + ELSEIF ch% = 92 THEN + retVal$ = retVal$ + CHR$(92) + ELSEIF ch% = 47 THEN + retVal$ = retVal$ + CHR$(47) + ELSEIF ch% = 110 THEN + retVal$ = retVal$ + CHR$(10) + ELSEIF ch% = 114 THEN + retVal$ = retVal$ + CHR$(13) + ELSEIF ch% = 116 THEN + retVal$ = retVal$ + CHR$(9) + ELSEIF ch% = 98 THEN + retVal$ = retVal$ + CHR$(8) + ELSEIF ch% = 102 THEN + retVal$ = retVal$ + CHR$(12) + ELSE + retVal$ = retVal$ + CHR$(ch%) + END IF + ELSE + retVal$ = retVal$ + CHR$(ch%) + END IF + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WEND + + IF ch% = 34 THEN + _jsPos& = _jsPos& + 1 + ELSE + _JsSetError("Unterminated string") + END IF + + _JsParseString$ = retVal$ +END SUB + +{* ============== Internal: Number scanning ============== *} + +SUB _JsScanNumber + SHARED _jsBase&, _jsPos&, _jsLen&, _jsErr$ + SHARED _jsNumBuf$, _jsNumIsFloat%, _jsNumInt& + SHORTINT ch%, hasDigit%, isNeg% + LONGINT i& + + IF LEN(_jsErr$) > 0 THEN EXIT SUB + + _jsNumBuf$ = "" + _jsNumIsFloat% = 0 + _jsNumInt& = 0 + hasDigit% = 0 + + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + ' Optional leading minus + IF ch% = 45 THEN + _jsNumBuf$ = "-" + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + END IF + + ' Digits before decimal + WHILE ch% >= 48 AND ch% <= 57 + _jsNumBuf$ = _jsNumBuf$ + CHR$(ch%) + hasDigit% = -1 + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WEND + + ' Optional decimal point + digits + IF ch% = 46 THEN + _jsNumIsFloat% = -1 + _jsNumBuf$ = _jsNumBuf$ + CHR$(46) + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WHILE ch% >= 48 AND ch% <= 57 + _jsNumBuf$ = _jsNumBuf$ + CHR$(ch%) + hasDigit% = -1 + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WEND + END IF + + ' Optional exponent + IF ch% = 101 OR ch% = 69 THEN + _jsNumIsFloat% = -1 + _jsNumBuf$ = _jsNumBuf$ + CHR$(ch%) + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + IF ch% = 43 OR ch% = 45 THEN + _jsNumBuf$ = _jsNumBuf$ + CHR$(ch%) + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + END IF + WHILE ch% >= 48 AND ch% <= 57 + _jsNumBuf$ = _jsNumBuf$ + CHR$(ch%) + _jsPos& = _jsPos& + 1 + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + WEND + END IF + + IF NOT hasDigit% THEN + _JsSetError("Invalid number") + EXIT SUB + END IF + + ' Parse integer value for non-float case + IF NOT _jsNumIsFloat% THEN + _jsNumInt& = 0 + isNeg% = 0 + i& = 1 + IF LEFT$(_jsNumBuf$, 1) = "-" THEN + isNeg% = -1 + i& = 2 + END IF + WHILE i& <= LEN(_jsNumBuf$) + _jsNumInt& = _jsNumInt& * 10 + (ASC(MID$(_jsNumBuf$, i&, 1)) - 48) + i& = i& + 1 + WEND + IF isNeg% THEN _jsNumInt& = -_jsNumInt& + END IF +END SUB + +{* ============== Internal: Recursive container parser ============== *} + +SUB LONGINT _JsParseContainer + SHARED _jsBase&, _jsPos&, _jsLen&, _jsErr$ + SHARED _jsHmDesc&, _jsDaDesc&, _jsDepth% + SHARED _jsNumBuf$, _jsNumIsFloat%, _jsNumInt& + SHARED _jsKeyStk$ + LONGINT retVal&, childAddr& + SHORTINT ch%, done% + STRING sVal$ SIZE 256 + DECLARE CLASS Hashmap hm + DECLARE CLASS DynArray da + + retVal& = 0 + + IF LEN(_jsErr$) > 0 THEN + _JsParseContainer = 0 + EXIT SUB + END IF + + _jsDepth% = _jsDepth% + 1 + IF _jsDepth% > JS_MAX_DEPTH THEN + _JsSetError("Max nesting depth exceeded") + _jsDepth% = _jsDepth% - 1 + _JsParseContainer = 0 + EXIT SUB + END IF + + ' Peek at opening character + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% = 123 THEN + {* ===== OBJECT ===== *} + + retVal& = ALLOC(_JS_HM_SIZE) + IF retVal& = 0 THEN + _JsSetError("Out of memory") + _jsDepth% = _jsDepth% - 1 + _JsParseContainer = 0 + EXIT SUB + END IF + POKEL retVal&, _jsHmDesc& + hm = retVal& + HmMake(hm, HM_MEDIUM) + + _jsPos& = _jsPos& + 1 + _JsSkipWs + + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% <> 125 THEN + done% = 0 + WHILE NOT done% AND LEN(_jsErr$) = 0 + _JsSkipWs + _jsKeyStk$(_jsDepth%) = _JsParseString$ + IF LEN(_jsErr$) > 0 THEN + done% = -1 + ELSE + _JsSkipWs + _JsExpect(58) + _JsSkipWs + + ' Dispatch object value + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% = 34 THEN + sVal$ = _JsParseString$ + IF LEN(_jsErr$) = 0 THEN + HmPut$(hm, _jsKeyStk$(_jsDepth%), sVal$) + END IF + ELSEIF ch% = 123 OR ch% = 91 THEN + childAddr& = _JsParseContainer + hm = retVal& + IF LEN(_jsErr$) = 0 THEN + HmPutRef(hm, _jsKeyStk$(_jsDepth%), childAddr&) + END IF + ELSEIF ch% = 116 THEN + IF _JsMatchLit("true") THEN + HmPutBool(hm, _jsKeyStk$(_jsDepth%), -1) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 102 THEN + IF _JsMatchLit("false") THEN + HmPutBool(hm, _jsKeyStk$(_jsDepth%), 0) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 110 THEN + IF _JsMatchLit("null") THEN + HmPutNull(hm, _jsKeyStk$(_jsDepth%)) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 45 OR (ch% >= 48 AND ch% <= 57) THEN + _JsScanNumber + IF LEN(_jsErr$) = 0 THEN + IF _jsNumIsFloat% THEN + HmPut!(hm, _jsKeyStk$(_jsDepth%), VAL(_jsNumBuf$)) + ELSE + HmPut&(hm, _jsKeyStk$(_jsDepth%), _jsNumInt&) + END IF + END IF + ELSE + _JsSetError("Unexpected character") + END IF + + _JsSkipWs + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + IF ch% = 44 THEN + _jsPos& = _jsPos& + 1 + ELSE + done% = -1 + END IF + END IF + WEND + END IF + + IF LEN(_jsErr$) = 0 THEN + _JsExpect(125) + END IF + + IF LEN(_jsErr$) > 0 THEN + HmFree(hm) + FREE retVal& + retVal& = 0 + END IF + + ELSEIF ch% = 91 THEN + {* ===== ARRAY ===== *} + + retVal& = ALLOC(_JS_DA_SIZE) + IF retVal& = 0 THEN + _JsSetError("Out of memory") + _jsDepth% = _jsDepth% - 1 + _JsParseContainer = 0 + EXIT SUB + END IF + POKEL retVal&, _jsDaDesc& + da = retVal& + DaMake(da, DA_MEDIUM) + + _jsPos& = _jsPos& + 1 + _JsSkipWs + + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% <> 93 THEN + done% = 0 + WHILE NOT done% AND LEN(_jsErr$) = 0 + _JsSkipWs + + ' Dispatch array value + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + + IF ch% = 34 THEN + sVal$ = _JsParseString$ + IF LEN(_jsErr$) = 0 THEN + DaAppend$(da, sVal$) + END IF + ELSEIF ch% = 123 OR ch% = 91 THEN + childAddr& = _JsParseContainer + da = retVal& + IF LEN(_jsErr$) = 0 THEN + DaAppendRef(da, childAddr&) + END IF + ELSEIF ch% = 116 THEN + IF _JsMatchLit("true") THEN + DaAppendBool(da, -1) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 102 THEN + IF _JsMatchLit("false") THEN + DaAppendBool(da, 0) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 110 THEN + IF _JsMatchLit("null") THEN + DaAppendNull(da) + ELSE + _JsSetError("Invalid literal") + END IF + ELSEIF ch% = 45 OR (ch% >= 48 AND ch% <= 57) THEN + _JsScanNumber + IF LEN(_jsErr$) = 0 THEN + IF _jsNumIsFloat% THEN + DaAppend!(da, VAL(_jsNumBuf$)) + ELSE + DaAppend&(da, _jsNumInt&) + END IF + END IF + ELSE + _JsSetError("Unexpected character") + END IF + + _JsSkipWs + IF _jsPos& < _jsLen& THEN + ch% = PEEK(_jsBase& + _jsPos&) + ELSE + ch% = -1 + END IF + IF ch% = 44 THEN + _jsPos& = _jsPos& + 1 + ELSE + done% = -1 + END IF + WEND + END IF + + IF LEN(_jsErr$) = 0 THEN + _JsExpect(93) + END IF + + IF LEN(_jsErr$) > 0 THEN + DaFree(da) + FREE retVal& + retVal& = 0 + END IF + + ELSE + _JsSetError("Expected { or [") + END IF + + _jsDepth% = _jsDepth% - 1 + _JsParseContainer = retVal& +END SUB + +{* ============== Public API ============== *} + +SUB LONGINT JsParse(src$) EXTERNAL + SHARED _jsBase&, _jsPos&, _jsLen&, _jsDepth%, _jsRootType%, _jsErr$ + LONGINT retVal& + SHORTINT ch% + retVal& = 0 + + _JsInitDesc + + _jsBase& = SADD(src$) + _jsPos& = 0 + _jsLen& = LEN(src$) + _jsDepth% = 0 + _jsErr$ = "" + + _JsSkipWs + ch% = _JsCh% + + IF ch% = 123 THEN + _jsRootType% = JsObject + ELSEIF ch% = 91 THEN + _jsRootType% = JsArray + ELSE + _JsSetError("Expected { or [") + JsParse = 0 + EXIT SUB + END IF + + retVal& = _JsParseContainer + + JsParse = retVal& +END SUB + +SUB LONGINT JsParseFile(SHORTINT ch%) EXTERNAL + SHARED _jsBase&, _jsPos&, _jsLen&, _jsDepth%, _jsRootType%, _jsErr$ + STRING buf$ SIZE 8192 + STRING ln$ SIZE 1024 + LONGINT retVal& + SHORTINT c% + + _JsInitDesc + + buf$ = "" + _jsErr$ = "" + retVal& = 0 + + WHILE NOT EOF(ch%) + LINE INPUT #ch%, ln$ + IF LEN(buf$) = 0 THEN + buf$ = ln$ + ELSEIF LEN(buf$) + LEN(ln$) + 1 <= JS_MAX_INPUT THEN + buf$ = buf$ + " " + ln$ + ELSE + _JsSetError("Input too large") + END IF + WEND + + IF LEN(_jsErr$) > 0 THEN + JsParseFile = 0 + EXIT SUB + END IF + + IF LEN(buf$) = 0 THEN + _JsSetError("Expected { or [") + JsParseFile = 0 + EXIT SUB + END IF + + _jsBase& = SADD(buf$) + _jsPos& = 0 + _jsLen& = LEN(buf$) + _jsDepth% = 0 + + _JsSkipWs + c% = _JsCh% + + IF c% = 123 THEN + _jsRootType% = JsObject + ELSEIF c% = 91 THEN + _jsRootType% = JsArray + ELSE + _JsSetError("Expected { or [") + JsParseFile = 0 + EXIT SUB + END IF + + retVal& = _JsParseContainer + JsParseFile = retVal& +END SUB + +{* ============== Generator: String escaping ============== *} + +SUB _JsWriteStr(s$, SHORTINT ch%) + STRING esc$ SIZE 600 + LONGINT i& + SHORTINT c% + esc$ = CHR$(34) + FOR i& = 1 TO LEN(s$) + c% = ASC(MID$(s$, i&, 1)) + IF c% = 34 THEN + esc$ = esc$ + "\" + CHR$(34) + ELSEIF c% = 92 THEN + esc$ = esc$ + "\\" + ELSEIF c% = 10 THEN + esc$ = esc$ + "\n" + ELSEIF c% = 13 THEN + esc$ = esc$ + "\r" + ELSEIF c% = 9 THEN + esc$ = esc$ + "\t" + ELSEIF c% = 8 THEN + esc$ = esc$ + "\b" + ELSEIF c% = 12 THEN + esc$ = esc$ + "\f" + ELSEIF c% < 32 THEN + ' skip other control chars + ELSE + esc$ = esc$ + CHR$(c%) + END IF + NEXT + esc$ = esc$ + CHR$(34) + PRINT #ch%, esc$; +END SUB + +{* ============== Generator: Recursive node writer ============== *} + +SUB _JsWriteNode(ADDRESS addr&, SHORTINT ch%) + LONGINT savedAddr&, i&, cnt& + SHORTINT first%, typ% + SINGLE fVal! + DECLARE CLASS Hashmap tcItem + + IF addr& = 0 THEN EXIT SUB + + savedAddr& = addr& + tcItem = addr& + + TYPECASE tcItem + CASE Hashmap + PRINT #ch%, "{"; + HmIterReset(tcItem) + first% = -1 + WHILE HmIterNext(tcItem) + IF NOT first% THEN PRINT #ch%, ","; + first% = 0 + _JsWriteStr(HmIterKey$(tcItem), ch%) + PRINT #ch%, ":"; + + typ% = HmIterType(tcItem) + IF typ% = HmTypeStr THEN + _JsWriteStr(HmIterVal$(tcItem), ch%) + ELSEIF typ% = HmTypeLng THEN + PRINT #ch%, LTRIM$(STR$(HmIterVal&(tcItem))); + ELSEIF typ% = HmTypeSng THEN + fVal! = HmIterVal!(tcItem) + PRINT #ch%, LTRIM$(STR$(fVal!)); + ELSEIF typ% = HmTypeBool THEN + IF HmIterVal&(tcItem) THEN + PRINT #ch%, "true"; + ELSE + PRINT #ch%, "false"; + END IF + ELSEIF typ% = HmTypeNull THEN + PRINT #ch%, "null"; + ELSEIF typ% = HmTypeRef THEN + _JsWriteNode(HmIterVal&(tcItem), ch%) + tcItem = savedAddr& + END IF + WEND + PRINT #ch%, "}"; + + CASE DynArray + cnt& = DaCount(tcItem) + PRINT #ch%, "["; + FOR i& = 0 TO cnt& - 1 + IF i& > 0 THEN PRINT #ch%, ","; + + typ% = DaType(tcItem, i&) + IF typ% = DaTypeStr THEN + _JsWriteStr(DaGet$(tcItem, i&), ch%) + ELSEIF typ% = DaTypeLng THEN + PRINT #ch%, LTRIM$(STR$(DaGet&(tcItem, i&))); + ELSEIF typ% = DaTypeSng THEN + fVal! = DaGet!(tcItem, i&) + PRINT #ch%, LTRIM$(STR$(fVal!)); + ELSEIF typ% = DaTypeBool THEN + IF DaGet&(tcItem, i&) THEN + PRINT #ch%, "true"; + ELSE + PRINT #ch%, "false"; + END IF + ELSEIF typ% = DaTypeNull THEN + PRINT #ch%, "null"; + ELSEIF typ% = DaTypeRef THEN + _JsWriteNode(DaGet&(tcItem, i&), ch%) + tcItem = savedAddr& + END IF + NEXT + PRINT #ch%, "]"; + END TYPECASE +END SUB + +{* ============== Generator: Formatted (pretty-print) node writer ============== *} + +SUB _JsWriteNodeFmt(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) + LONGINT savedAddr&, i&, cnt& + SHORTINT first%, typ%, pad% + SINGLE fVal! + DECLARE CLASS Hashmap tcItem + + IF addr& = 0 THEN EXIT SUB + + savedAddr& = addr& + tcItem = addr& + + TYPECASE tcItem + CASE Hashmap + PRINT #ch%, "{"; + HmIterReset(tcItem) + first% = -1 + WHILE HmIterNext(tcItem) + IF NOT first% THEN + PRINT #ch%, ","; + END IF + PRINT #ch%, CHR$(10); + first% = 0 + pad% = (lvl% + 1) * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); + _JsWriteStr(HmIterKey$(tcItem), ch%) + PRINT #ch%, ": "; + + typ% = HmIterType(tcItem) + IF typ% = HmTypeStr THEN + _JsWriteStr(HmIterVal$(tcItem), ch%) + ELSEIF typ% = HmTypeLng THEN + PRINT #ch%, LTRIM$(STR$(HmIterVal&(tcItem))); + ELSEIF typ% = HmTypeSng THEN + fVal! = HmIterVal!(tcItem) + PRINT #ch%, LTRIM$(STR$(fVal!)); + ELSEIF typ% = HmTypeBool THEN + IF HmIterVal&(tcItem) THEN + PRINT #ch%, "true"; + ELSE + PRINT #ch%, "false"; + END IF + ELSEIF typ% = HmTypeNull THEN + PRINT #ch%, "null"; + ELSEIF typ% = HmTypeRef THEN + _JsWriteNodeFmt(HmIterVal&(tcItem), ch%, lvl% + 1) + tcItem = savedAddr& + END IF + WEND + IF NOT first% THEN + PRINT #ch%, CHR$(10); + pad% = lvl% * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); + END IF + PRINT #ch%, "}"; + + CASE DynArray + cnt& = DaCount(tcItem) + PRINT #ch%, "["; + FOR i& = 0 TO cnt& - 1 + IF i& > 0 THEN + PRINT #ch%, ","; + END IF + PRINT #ch%, CHR$(10); + pad% = (lvl% + 1) * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); + + typ% = DaType(tcItem, i&) + IF typ% = DaTypeStr THEN + _JsWriteStr(DaGet$(tcItem, i&), ch%) + ELSEIF typ% = DaTypeLng THEN + PRINT #ch%, LTRIM$(STR$(DaGet&(tcItem, i&))); + ELSEIF typ% = DaTypeSng THEN + fVal! = DaGet!(tcItem, i&) + PRINT #ch%, LTRIM$(STR$(fVal!)); + ELSEIF typ% = DaTypeBool THEN + IF DaGet&(tcItem, i&) THEN + PRINT #ch%, "true"; + ELSE + PRINT #ch%, "false"; + END IF + ELSEIF typ% = DaTypeNull THEN + PRINT #ch%, "null"; + ELSEIF typ% = DaTypeRef THEN + _JsWriteNodeFmt(DaGet&(tcItem, i&), ch%, lvl% + 1) + tcItem = savedAddr& + END IF + NEXT + IF cnt& > 0 THEN + PRINT #ch%, CHR$(10); + pad% = lvl% * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); + END IF + PRINT #ch%, "]"; + END TYPECASE +END SUB + +{* ============== Public API: Generator ============== *} + +SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL + IF root& = 0 THEN EXIT SUB + _JsWriteNode(root&, ch%) +END SUB + +SUB JsWriteFmt(ADDRESS root&, SHORTINT ch%) EXTERNAL + IF root& = 0 THEN EXIT SUB + _JsWriteNodeFmt(root&, ch%, 0) +END SUB + +SUB STRING JsToStr$(ADDRESS root&) EXTERNAL + STRING retVal$ SIZE 4096 + retVal$ = "" + IF root& = 0 THEN + JsToStr$ = retVal$ + EXIT SUB + END IF + + OPEN "O", #9, "T:js_tmp" + JsWrite(root&, 9) + CLOSE #9 + + OPEN "I", #9, "T:js_tmp" + LINE INPUT #9, retVal$ + CLOSE #9 + + IF LEN(retVal$) > 4000 THEN + retVal$ = "" + END IF + + JsToStr$ = retVal$ +END SUB + +{* ============== Public API: Info ============== *} + +SUB STRING JsError$ EXTERNAL + SHARED _jsErr$ + JsError$ = _jsErr$ +END SUB + +SUB SHORTINT JsRootType EXTERNAL + SHARED _jsRootType% + JsRootType = _jsRootType% +END SUB + +{* ============== Public API: Cleanup ============== *} + +SUB JsFree(ADDRESS root&) EXTERNAL + LONGINT savedAddr&, i&, cnt&, childAddr& + DECLARE CLASS Hashmap tcItem + + IF root& = 0 THEN EXIT SUB + + savedAddr& = root& + tcItem = root& + + TYPECASE tcItem + CASE Hashmap + HmIterReset(tcItem) + WHILE HmIterNext(tcItem) + IF HmIterType(tcItem) = HmTypeRef THEN + childAddr& = HmIterVal&(tcItem) + JsFree(childAddr&) + tcItem = savedAddr& + END IF + WEND + HmFree(tcItem) + FREE root& + + CASE DynArray + cnt& = DaCount(tcItem) + FOR i& = 0 TO cnt& - 1 + IF DaType(tcItem, i&) = DaTypeRef THEN + childAddr& = DaGet&(tcItem, i&) + JsFree(childAddr&) + tcItem = savedAddr& + END IF + NEXT + DaFree(tcItem) + FREE root& + END TYPECASE +END SUB + +{* ============== Public API: Convenience ============== *} + +SUB JsMakeObj(Hashmap hm, LONGINT cap&) EXTERNAL + HmMake(hm, cap&) +END SUB + +SUB JsMakeArr(DynArray da, LONGINT cap&) EXTERNAL + DaMake(da, cap&) +END SUB diff --git a/submods/json/make b/submods/json/make new file mode 100644 index 0000000..a2a896b --- /dev/null +++ b/submods/json/make @@ -0,0 +1,2 @@ +; Build json module +execute ACE:bin/bas -mO json diff --git a/submods/json/test_free.b b/submods/json/test_free.b new file mode 100644 index 0000000..50258ce --- /dev/null +++ b/submods/json/test_free.b @@ -0,0 +1,188 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_free.b - Phase 6: JsFree + JsMakeObj/JsMakeArr +** Tests: recursive free, convenience constructors, null safety +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 6: JsFree + Convenience ===" +PRINT + +TkInit + +STRING src$ SIZE 4096 +STRING q$ SIZE 4 +q$ = CHR$(34) + +LONGINT root& + +{* ============== Test 1: JsFree null safety ============== *} + +PRINT "-- Test 1: JsFree null safety" +JsFree(0) +TkAssertTrue(-1, "JsFree(0) no crash") + +{* ============== Test 2: JsFree simple object ============== *} + +PRINT "-- Test 2: JsFree simple object" +src$ = "{" + q$ + "a" + q$ + ":" + q$ + "b" + q$ + "}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free obj parse") +JsFree(root&) +TkAssertTrue(-1, "free obj no crash") + +{* ============== Test 3: JsFree simple array ============== *} + +PRINT "-- Test 3: JsFree simple array" +src$ = "[1,2,3]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free arr parse") +JsFree(root&) +TkAssertTrue(-1, "free arr no crash") + +{* ============== Test 4: JsFree empty object ============== *} + +PRINT "-- Test 4: JsFree empty object" +src$ = "{}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free {} parse") +JsFree(root&) +TkAssertTrue(-1, "free {} no crash") + +{* ============== Test 5: JsFree empty array ============== *} + +PRINT "-- Test 5: JsFree empty array" +src$ = "[]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free [] parse") +JsFree(root&) +TkAssertTrue(-1, "free [] no crash") + +{* ============== Test 6: JsFree nested object ============== *} + +PRINT "-- Test 6: JsFree nested object" +src$ = "{" + q$ + "inner" + q$ + ":{" + q$ + "x" + q$ + ":1}}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free nested parse") +JsFree(root&) +TkAssertTrue(-1, "free nested no crash") + +{* ============== Test 7: JsFree nested array in object ============== *} + +PRINT "-- Test 7: JsFree nested array in object" +src$ = "{" + q$ + "arr" + q$ + ":[10,20,30]}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free arr-in-obj parse") +JsFree(root&) +TkAssertTrue(-1, "free arr-in-obj no crash") + +{* ============== Test 8: JsFree deeply nested ============== *} + +PRINT "-- Test 8: JsFree deeply nested" +' {"d":[{"v":7,"sub":{"k":"val"}},true]} +src$ = "{" + q$ + "d" + q$ + ":[{" + q$ + "v" + q$ + ":7," +src$ = src$ + q$ + "sub" + q$ + ":{" + q$ + "k" + q$ + ":" +src$ = src$ + q$ + "val" + q$ + "}},true]}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free deep parse") +JsFree(root&) +TkAssertTrue(-1, "free deep no crash") + +{* ============== Test 9: JsFree array of arrays ============== *} + +PRINT "-- Test 9: JsFree array of arrays" +src$ = "[[1,2],[3,4]]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free arr-arr parse") +JsFree(root&) +TkAssertTrue(-1, "free arr-arr no crash") + +{* ============== Test 10: JsFree mixed types ============== *} + +PRINT "-- Test 10: JsFree mixed types" +src$ = "{" + q$ + "s" + q$ + ":" + q$ + "hi" + q$ + "," +src$ = src$ + q$ + "i" + q$ + ":42," +src$ = src$ + q$ + "b" + q$ + ":true," +src$ = src$ + q$ + "n" + q$ + ":null," +src$ = src$ + q$ + "a" + q$ + ":[1]}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "free mixed parse") +JsFree(root&) +TkAssertTrue(-1, "free mixed no crash") + +{* ============== Test 11: JsMakeObj produces TYPECASE-able instance ============== *} + +PRINT "-- Test 11: JsMakeObj TYPECASE" +DECLARE CLASS Hashmap hm +JsMakeObj(hm, HM_SMALL) +HmPut$(hm, "test", "ok") +SHORTINT isObj% +isObj% = 0 + +DECLARE CLASS Hashmap tc +tc = hm +TYPECASE tc + CASE Hashmap + isObj% = -1 +END TYPECASE + +TkAssertTrue(isObj%, "JsMakeObj typecase") +TkAssertEqStr(HmGet$(hm, "test"), "ok", "JsMakeObj val") +HmFree(hm) + +{* ============== Test 12: JsMakeArr produces TYPECASE-able instance ============== *} + +PRINT "-- Test 12: JsMakeArr TYPECASE" +DECLARE CLASS DynArray da +JsMakeArr(da, DA_SMALL) +DaAppend&(da, 77) +SHORTINT isArr% +isArr% = 0 + +tc = da +TYPECASE tc + CASE DynArray + isArr% = -1 +END TYPECASE + +TkAssertTrue(isArr%, "JsMakeArr typecase") +TkAssertEq&(DaGet&(da, 0), 77, "JsMakeArr val") +DaFree(da) + +{* ============== Test 13: Parse + use + JsFree full cycle ============== *} + +PRINT "-- Test 13: Parse + use + JsFree full cycle" +src$ = "{" + q$ + "msg" + q$ + ":" + q$ + "hello" + q$ + "," +src$ = src$ + q$ + "nums" + q$ + ":[1,2,3]}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "cycle parse") + +' Access values before freeing +DECLARE CLASS Hashmap doc +doc = root& +TkAssertEqStr(HmGet$(doc, "msg"), "hello", "cycle msg") + +LONGINT ref& +ref& = HmGetRef(doc, "nums") +TkAssertTrue(ref& <> 0, "cycle nums ref") + +DECLARE CLASS DynArray nums +nums = ref& +TkAssertEq&(DaCount(nums), 3, "cycle nums count") +TkAssertEq&(DaGet&(nums, 0), 1, "cycle nums[0]") +TkAssertEq&(DaGet&(nums, 2), 3, "cycle nums[2]") + +' Now free the entire tree +JsFree(root&) +TkAssertTrue(-1, "cycle free no crash") + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_gen.b b/submods/json/test_gen.b new file mode 100644 index 0000000..09fa664 --- /dev/null +++ b/submods/json/test_gen.b @@ -0,0 +1,290 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_gen.b - Phase 4: Generate compact JSON output +** Tests: JsWrite, JsToStr$ +** Builds data structures manually and verifies generated output. +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 4: Generate Compact JSON ===" +PRINT + +TkInit + +STRING result$ SIZE 4096 +STRING expected$ SIZE 4096 +STRING q$ SIZE 4 +q$ = CHR$(34) + +DECLARE CLASS Hashmap hm +DECLARE CLASS DynArray da + +{* ============== Test 1: Empty object ============== *} + +PRINT "-- Test 1: Empty object" +HmMake(hm, HM_SMALL) +result$ = JsToStr$(hm) +TkAssertEqStr(result$, "{}", "empty obj = {}") +HmFree(hm) + +{* ============== Test 2: Single string ============== *} + +PRINT "-- Test 2: Single string" +HmMake(hm, HM_SMALL) +HmPut$(hm, "city", "Berlin") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "city" + q$ + ":" + q$ + "Berlin" + q$ + "}" +TkAssertEqStr(result$, expected$, "single str obj") +HmFree(hm) + +{* ============== Test 3: Integer value ============== *} + +PRINT "-- Test 3: Integer value" +HmMake(hm, HM_SMALL) +HmPut&(hm, "age", 30) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "age" + q$ + ":30}" +TkAssertEqStr(result$, expected$, "int val obj") +HmFree(hm) + +{* ============== Test 4: Negative integer ============== *} + +PRINT "-- Test 4: Negative integer" +HmMake(hm, HM_SMALL) +HmPut&(hm, "n", -5) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "n" + q$ + ":-5}" +TkAssertEqStr(result$, expected$, "neg int obj") +HmFree(hm) + +{* ============== Test 5: Boolean true ============== *} + +PRINT "-- Test 5: Boolean true" +HmMake(hm, HM_SMALL) +HmPutBool(hm, "flag", -1) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "flag" + q$ + ":true}" +TkAssertEqStr(result$, expected$, "bool true obj") +HmFree(hm) + +{* ============== Test 6: Boolean false ============== *} + +PRINT "-- Test 6: Boolean false" +HmMake(hm, HM_SMALL) +HmPutBool(hm, "flag", 0) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "flag" + q$ + ":false}" +TkAssertEqStr(result$, expected$, "bool false obj") +HmFree(hm) + +{* ============== Test 7: Null value ============== *} + +PRINT "-- Test 7: Null value" +HmMake(hm, HM_SMALL) +HmPutNull(hm, "x") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "x" + q$ + ":null}" +TkAssertEqStr(result$, expected$, "null val obj") +HmFree(hm) + +{* ============== Test 8: Multiple values ============== *} + +PRINT "-- Test 8: Multiple values" +HmMake(hm, HM_SMALL) +HmPut$(hm, "a", "x") +HmPut&(hm, "b", 1) +HmPutBool(hm, "c", -1) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "a" + q$ + ":" + q$ + "x" + q$ + "," +expected$ = expected$ + q$ + "b" + q$ + ":1," +expected$ = expected$ + q$ + "c" + q$ + ":true}" +TkAssertEqStr(result$, expected$, "multi val obj") +HmFree(hm) + +{* ============== Test 9: Empty array ============== *} + +PRINT "-- Test 9: Empty array" +DaMake(da, DA_SMALL) +result$ = JsToStr$(da) +TkAssertEqStr(result$, "[]", "empty arr = []") +DaFree(da) + +{* ============== Test 10: String array ============== *} + +PRINT "-- Test 10: String array" +DaMake(da, DA_SMALL) +DaAppend$(da, "aaa") +DaAppend$(da, "bbb") +result$ = JsToStr$(da) +expected$ = "[" + q$ + "aaa" + q$ + "," + q$ + "bbb" + q$ + "]" +TkAssertEqStr(result$, expected$, "str array") +DaFree(da) + +{* ============== Test 11: Integer array ============== *} + +PRINT "-- Test 11: Integer array" +DaMake(da, DA_SMALL) +DaAppend&(da, 1) +DaAppend&(da, -2) +DaAppend&(da, 0) +result$ = JsToStr$(da) +TkAssertEqStr(result$, "[1,-2,0]", "int array") +DaFree(da) + +{* ============== Test 12: Mixed type array ============== *} + +PRINT "-- Test 12: Mixed type array" +DaMake(da, DA_SMALL) +DaAppend$(da, "hi") +DaAppend&(da, 42) +DaAppendBool(da, -1) +DaAppendNull(da) +result$ = JsToStr$(da) +expected$ = "[" + q$ + "hi" + q$ + ",42,true,null]" +TkAssertEqStr(result$, expected$, "mixed array") +DaFree(da) + +{* ============== Test 13: Nested object ============== *} + +PRINT "-- Test 13: Nested object" +DECLARE CLASS Hashmap inner +HmMake(inner, HM_SMALL) +HmPut&(inner, "x", 1) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "inner", inner) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "inner" + q$ + ":{" + q$ + "x" + q$ + ":1}}" +TkAssertEqStr(result$, expected$, "nested obj") +HmFree(inner) +HmFree(hm) + +{* ============== Test 14: Nested array in object ============== *} + +PRINT "-- Test 14: Nested array in object" +DaMake(da, DA_SMALL) +DaAppend&(da, 10) +DaAppend&(da, 20) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "nums", da) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "nums" + q$ + ":[10,20]}" +TkAssertEqStr(result$, expected$, "arr in obj") +DaFree(da) +HmFree(hm) + +{* ============== Test 15: Nested object in array ============== *} + +PRINT "-- Test 15: Nested object in array" +HmMake(inner, HM_SMALL) +HmPut$(inner, "k", "v") +DaMake(da, DA_SMALL) +DaAppendRef(da, inner) +result$ = JsToStr$(da) +expected$ = "[{" + q$ + "k" + q$ + ":" + q$ + "v" + q$ + "}]" +TkAssertEqStr(result$, expected$, "obj in arr") +HmFree(inner) +DaFree(da) + +{* ============== Test 16: String escaping - quote ============== *} + +PRINT "-- Test 16: String escaping - quote" +HmMake(hm, HM_SMALL) +HmPut$(hm, "k", "a" + CHR$(34) + "b") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "k" + q$ + ":" + q$ + "a\" + q$ + "b" + q$ + "}" +TkAssertEqStr(result$, expected$, "escape quote") +HmFree(hm) + +{* ============== Test 17: String escaping - backslash ============== *} + +PRINT "-- Test 17: String escaping - backslash" +HmMake(hm, HM_SMALL) +HmPut$(hm, "k", "a\b") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "k" + q$ + ":" + q$ + "a\\b" + q$ + "}" +TkAssertEqStr(result$, expected$, "escape backslash") +HmFree(hm) + +{* ============== Test 18: String escaping - newline ============== *} + +PRINT "-- Test 18: String escaping - newline" +HmMake(hm, HM_SMALL) +HmPut$(hm, "k", "a" + CHR$(10) + "b") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "k" + q$ + ":" + q$ + "a\nb" + q$ + "}" +TkAssertEqStr(result$, expected$, "escape newline") +HmFree(hm) + +{* ============== Test 19: String escaping - tab ============== *} + +PRINT "-- Test 19: String escaping - tab" +HmMake(hm, HM_SMALL) +HmPut$(hm, "k", "a" + CHR$(9) + "b") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "k" + q$ + ":" + q$ + "a\tb" + q$ + "}" +TkAssertEqStr(result$, expected$, "escape tab") +HmFree(hm) + +{* ============== Test 20: JsWrite to file ============== *} + +PRINT "-- Test 20: JsWrite to file" +HmMake(hm, HM_SMALL) +HmPut$(hm, "msg", "ok") +OPEN "O", #1, "T:js_test.json" +JsWrite(hm, 1) +CLOSE #1 +STRING fLine$ SIZE 1024 +OPEN "I", #1, "T:js_test.json" +LINE INPUT #1, fLine$ +CLOSE #1 +expected$ = "{" + q$ + "msg" + q$ + ":" + q$ + "ok" + q$ + "}" +TkAssertEqStr(fLine$, expected$, "JsWrite to file") +HmFree(hm) + +{* ============== Test 21: Deeply nested (obj > arr > obj) ============== *} + +PRINT "-- Test 21: Deeply nested" +DECLARE CLASS Hashmap deep +HmMake(deep, HM_SMALL) +HmPut&(deep, "v", 7) +DaMake(da, DA_SMALL) +DaAppendRef(da, deep) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "arr", da) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "arr" + q$ + ":[{" + q$ + "v" + q$ + ":7}]}" +TkAssertEqStr(result$, expected$, "deep nested") +HmFree(deep) +DaFree(da) +HmFree(hm) + +{* ============== Test 22: Key escaping ============== *} + +PRINT "-- Test 22: Key escaping" +HmMake(hm, HM_SMALL) +HmPut&(hm, "a" + CHR$(34) + "b", 1) +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "a\" + q$ + "b" + q$ + ":1}" +TkAssertEqStr(result$, expected$, "key with quote") +HmFree(hm) + +{* ============== Test 23: Empty string value ============== *} + +PRINT "-- Test 23: Empty string value" +HmMake(hm, HM_SMALL) +HmPut$(hm, "e", "") +result$ = JsToStr$(hm) +expected$ = "{" + q$ + "e" + q$ + ":" + q$ + q$ + "}" +TkAssertEqStr(result$, expected$, "empty str val") +HmFree(hm) + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_parse_arr.b b/submods/json/test_parse_arr.b new file mode 100644 index 0000000..02706c9 --- /dev/null +++ b/submods/json/test_parse_arr.b @@ -0,0 +1,535 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_parse_arr.b - Phase 2: Parse JSON arrays, nesting, floats +** Tests: arrays, nested objects/arrays, float values, TYPECASE, depth limit +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 2: Parse Arrays, Nesting, Floats ===" +PRINT + +TkInit + +STRING src$ SIZE 4096 +STRING errMsg$ SIZE 128 +STRING q$ SIZE 4 +q$ = CHR$(34) + +LONGINT root&, ref& +DECLARE CLASS Hashmap hm +DECLARE CLASS DynArray da +DECLARE CLASS DynArray innerDa +DECLARE CLASS Hashmap innerHm +SHORTINT matched% + +{* ============== Test 1: Empty array ============== *} + +PRINT "-- Test 1: Empty array" +src$ = "[]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse [] not null") +IF root& <> 0 THEN + TkAssertEq%(JsRootType, JsArray, "root type = array") + da = root& + TkAssertEq&(DaCount(da), 0, "empty count = 0") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 2: Array of strings ============== *} + +PRINT "-- Test 2: Array of strings" +src$ = "[" + q$ + "aaa" + q$ + "," + q$ + "bbb" + q$ + "," + q$ + "ccc" + q$ + "]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse str array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 3, "str arr count = 3") + TkAssertEqStr(DaGet$(da, 0), "aaa", "elem 0 = aaa") + TkAssertEqStr(DaGet$(da, 1), "bbb", "elem 1 = bbb") + TkAssertEqStr(DaGet$(da, 2), "ccc", "elem 2 = ccc") + TkAssertEq%(DaType(da, 0), DaTypeStr, "elem 0 type = str") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 3: Array of integers ============== *} + +PRINT "-- Test 3: Array of integers" +src$ = "[10, -20, 0, 999]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse int array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 4, "int arr count = 4") + TkAssertEq&(DaGet&(da, 0), 10, "elem 0 = 10") + TkAssertEq&(DaGet&(da, 1), -20, "elem 1 = -20") + TkAssertEq&(DaGet&(da, 2), 0, "elem 2 = 0") + TkAssertEq&(DaGet&(da, 3), 999, "elem 3 = 999") + TkAssertEq%(DaType(da, 0), DaTypeLng, "elem 0 type = lng") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 4: Array of booleans ============== *} + +PRINT "-- Test 4: Array of booleans" +src$ = "[true, false, true]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse bool array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 3, "bool arr count = 3") + TkAssertEq%(DaType(da, 0), DaTypeBool, "elem 0 type = bool") + TkAssertEq%(DaType(da, 1), DaTypeBool, "elem 1 type = bool") + TkAssertEq&(DaGet&(da, 0), 1, "elem 0 = 1 (true)") + TkAssertEq&(DaGet&(da, 1), 0, "elem 1 = 0 (false)") + TkAssertEq&(DaGet&(da, 2), 1, "elem 2 = 1 (true)") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 5: Array with nulls ============== *} + +PRINT "-- Test 5: Array with nulls" +src$ = "[null, null]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse null array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 2, "null arr count = 2") + TkAssertEq%(DaType(da, 0), DaTypeNull, "elem 0 type = null") + TkAssertEq%(DaType(da, 1), DaTypeNull, "elem 1 type = null") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 6: Mixed-type array ============== *} + +PRINT "-- Test 6: Mixed-type array" +src$ = "[" + q$ + "hi" + q$ + ", 42, true, null]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse mixed array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 4, "mixed arr count = 4") + TkAssertEq%(DaType(da, 0), DaTypeStr, "elem 0 type = str") + TkAssertEq%(DaType(da, 1), DaTypeLng, "elem 1 type = lng") + TkAssertEq%(DaType(da, 2), DaTypeBool, "elem 2 type = bool") + TkAssertEq%(DaType(da, 3), DaTypeNull, "elem 3 type = null") + TkAssertEqStr(DaGet$(da, 0), "hi", "elem 0 = hi") + TkAssertEq&(DaGet&(da, 1), 42, "elem 1 = 42") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 7: Nested object inside array ============== *} + +PRINT "-- Test 7: Nested object inside array" +src$ = "[{" + q$ + "nm" + q$ + ":" + q$ + "Bob" + q$ + "}]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse arr with obj") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 1, "arr count = 1") + TkAssertEq%(DaType(da, 0), DaTypeRef, "elem 0 type = ref") + ref& = DaGetRef(da, 0) + TkAssertNeq&(ref&, 0, "ref not null") + IF ref& <> 0 THEN + innerHm = ref& + TkAssertEqStr(HmGet$(innerHm, "nm"), "Bob", "nm = Bob") + HmFree(innerHm) + FREE ref& + END IF + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 8: Nested array inside object ============== *} + +PRINT "-- Test 8: Nested array inside object" +src$ = "{" + q$ + "tg" + q$ + ":[" + q$ + "dev" + q$ + "," +src$ = src$ + q$ + "amg" + q$ + "]}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse obj with arr") +IF root& <> 0 THEN + hm = root& + TkAssertEq%(HmType(hm, "tg"), HmTypeRef, "tg type = ref") + ref& = HmGetRef(hm, "tg") + TkAssertNeq&(ref&, 0, "tg ref not null") + IF ref& <> 0 THEN + innerDa = ref& + TkAssertEq&(DaCount(innerDa), 2, "inner count = 2") + TkAssertEqStr(DaGet$(innerDa, 0), "dev", "tag 0 = dev") + TkAssertEqStr(DaGet$(innerDa, 1), "amg", "tag 1 = amg") + DaFree(innerDa) + FREE ref& + END IF + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 9: Nested array inside array ============== *} + +PRINT "-- Test 9: Nested array inside array" +src$ = "[[1, 2], [3, 4]]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse nested arrays") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 2, "outer count = 2") + TkAssertEq%(DaType(da, 0), DaTypeRef, "elem 0 type = ref") + TkAssertEq%(DaType(da, 1), DaTypeRef, "elem 1 type = ref") + + ref& = DaGetRef(da, 0) + IF ref& <> 0 THEN + innerDa = ref& + TkAssertEq&(DaCount(innerDa), 2, "inner0 count = 2") + TkAssertEq&(DaGet&(innerDa, 0), 1, "inner0[0] = 1") + TkAssertEq&(DaGet&(innerDa, 1), 2, "inner0[1] = 2") + DaFree(innerDa) + FREE ref& + END IF + + ref& = DaGetRef(da, 1) + IF ref& <> 0 THEN + innerDa = ref& + TkAssertEq&(DaCount(innerDa), 2, "inner1 count = 2") + TkAssertEq&(DaGet&(innerDa, 0), 3, "inner1[0] = 3") + TkAssertEq&(DaGet&(innerDa, 1), 4, "inner1[1] = 4") + DaFree(innerDa) + FREE ref& + END IF + + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 10: Deeply nested structure ============== *} + +PRINT "-- Test 10: Deeply nested structure" +' {"a":{"b":{"c":42}}} +src$ = "{" + q$ + "a" + q$ + ":{" + q$ + "b" + q$ + ":{" + q$ + "c" + q$ + ":42}}}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse deep nested") +IF root& <> 0 THEN + hm = root& + ref& = HmGetRef(hm, "a") + TkAssertNeq&(ref&, 0, "a ref not null") + IF ref& <> 0 THEN + DECLARE CLASS Hashmap midHm + midHm = ref& + LONGINT ref2& + ref2& = HmGetRef(midHm, "b") + TkAssertNeq&(ref2&, 0, "b ref not null") + IF ref2& <> 0 THEN + DECLARE CLASS Hashmap deepHm + deepHm = ref2& + TkAssertEq&(HmGet&(deepHm, "c"), 42, "c = 42") + HmFree(deepHm) + FREE ref2& + END IF + HmFree(midHm) + FREE ref& + END IF + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 11: Float values ============== *} + +PRINT "-- Test 11: Float values" +src$ = "{" + q$ + "pi" + q$ + ":3.14," +src$ = src$ + q$ + "ng" + q$ + ":-0.5," +src$ = src$ + q$ + "ex" + q$ + ":1.5e2}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse floats") +IF root& <> 0 THEN + hm = root& + TkAssertEq%(HmType(hm, "pi"), HmTypeSng, "pi type = sng") + TkAssertEq%(HmType(hm, "ng"), HmTypeSng, "ng type = sng") + TkAssertEq%(HmType(hm, "ex"), HmTypeSng, "ex type = sng") + + ' FFP precision is ~7 digits, use approximate checks + SINGLE piVal!, ngVal!, exVal! + piVal! = HmGet!(hm, "pi") + ngVal! = HmGet!(hm, "ng") + exVal! = HmGet!(hm, "ex") + + ' Check pi is roughly 3.14 + TkAssertTrue(piVal! > 3.0 AND piVal! < 3.2, "pi ~ 3.14") + ' Check ng is roughly -0.5 + TkAssertTrue(ngVal! < 0.0, "ng < 0") + ' Check ex is roughly 150 + TkAssertTrue(exVal! > 140.0 AND exVal! < 160.0, "ex ~ 150") + + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 12: Float values in array ============== *} + +PRINT "-- Test 12: Float values in array" +src$ = "[1.5, -2.7, 0.001]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse float array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 3, "float arr count = 3") + TkAssertEq%(DaType(da, 0), DaTypeSng, "elem 0 type = sng") + TkAssertEq%(DaType(da, 1), DaTypeSng, "elem 1 type = sng") + TkAssertEq%(DaType(da, 2), DaTypeSng, "elem 2 type = sng") + + SINGLE fv! + fv! = DaGet!(da, 0) + TkAssertTrue(fv! > 1.0 AND fv! < 2.0, "elem 0 ~ 1.5") + fv! = DaGet!(da, 1) + TkAssertTrue(fv! < -2.0 AND fv! > -3.0, "elem 1 ~ -2.7") + + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 13: TYPECASE on nested refs ============== *} + +PRINT "-- Test 13: TYPECASE on nested refs" +' {"obj":{"x":1},"arr":[2]} +src$ = "{" + q$ + "obj" + q$ + ":{" + q$ + "x" + q$ + ":1}," +src$ = src$ + q$ + "arr" + q$ + ":[2]}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse for typecase") +IF root& <> 0 THEN + hm = root& + + ' Check nested object via TYPECASE + ref& = HmGetRef(hm, "obj") + matched% = 0 + DECLARE CLASS Hashmap tcHm + tcHm = ref& + TYPECASE tcHm + CASE Hashmap + matched% = 1 + CASE DynArray + matched% = 2 + CASE ELSE + matched% = -1 + END TYPECASE + TkAssertEq%(matched%, 1, "obj ref = Hashmap") + + ' Check nested array via TYPECASE + ref& = HmGetRef(hm, "arr") + matched% = 0 + DECLARE CLASS Hashmap tcProbe + tcProbe = ref& + TYPECASE tcProbe + CASE Hashmap + matched% = 1 + CASE DynArray + matched% = 2 + CASE ELSE + matched% = -1 + END TYPECASE + TkAssertEq%(matched%, 2, "arr ref = DynArray") + + ' Clean up nested refs + ref& = HmGetRef(hm, "obj") + IF ref& <> 0 THEN + innerHm = ref& + HmFree(innerHm) + FREE ref& + END IF + ref& = HmGetRef(hm, "arr") + IF ref& <> 0 THEN + innerDa = ref& + DaFree(innerDa) + FREE ref& + END IF + + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 14: TYPECASE with narrowing ============== *} + +PRINT "-- Test 14: TYPECASE with narrowing" +' [{"k":99}] +src$ = "[{" + q$ + "k" + q$ + ":99}]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse for narrowing") +IF root& <> 0 THEN + da = root& + ref& = DaGetRef(da, 0) + DECLARE CLASS Hashmap nrProbe + nrProbe = ref& + LONGINT gotK& + gotK& = 0 + + TYPECASE nrProbe + CASE Hashmap + ' Narrowed: nrProbe is treated as Hashmap here + gotK& = HmGet&(nrProbe, "k") + CASE DynArray + gotK& = -1 + END TYPECASE + + TkAssertEq&(gotK&, 99, "narrowed k = 99") + + ' Cleanup + IF ref& <> 0 THEN + innerHm = ref& + HmFree(innerHm) + FREE ref& + END IF + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 15: Complex nested structure ============== *} + +PRINT "-- Test 15: Complex nested structure" +' {"items":[{"id":1},{"id":2}],"total":2} +src$ = "{" + q$ + "items" + q$ + ":[{" +src$ = src$ + q$ + "id" + q$ + ":1},{" +src$ = src$ + q$ + "id" + q$ + ":2}]," +src$ = src$ + q$ + "total" + q$ + ":2}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse complex nested") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmGet&(hm, "total"), 2, "total = 2") + ref& = HmGetRef(hm, "items") + TkAssertNeq&(ref&, 0, "items ref not null") + IF ref& <> 0 THEN + innerDa = ref& + TkAssertEq&(DaCount(innerDa), 2, "items count = 2") + + LONGINT itemRef& + itemRef& = DaGetRef(innerDa, 0) + IF itemRef& <> 0 THEN + DECLARE CLASS Hashmap item0 + item0 = itemRef& + TkAssertEq&(HmGet&(item0, "id"), 1, "item 0 id = 1") + HmFree(item0) + FREE itemRef& + END IF + + itemRef& = DaGetRef(innerDa, 1) + IF itemRef& <> 0 THEN + DECLARE CLASS Hashmap item1 + item1 = itemRef& + TkAssertEq&(HmGet&(item1, "id"), 2, "item 1 id = 2") + HmFree(item1) + FREE itemRef& + END IF + + DaFree(innerDa) + FREE ref& + END IF + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 16: Depth limit error ============== *} + +PRINT "-- Test 16: Depth limit error" +' Build 11 levels of nesting: [[[[[[[[[[[ ]]]]]]]]]]] +src$ = "[[[[[[[[[[[" +src$ = src$ + q$ + "deep" + q$ +src$ = src$ + "]]]]]]]]]]]" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "depth limit -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "depth limit -> error msg") + +{* ============== Test 17: Whitespace in arrays ============== *} + +PRINT "-- Test 17: Whitespace in arrays" +src$ = " [ 1 , 2 , 3 ] " +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse ws array") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 3, "ws arr count = 3") + TkAssertEq&(DaGet&(da, 0), 1, "ws elem 0 = 1") + TkAssertEq&(DaGet&(da, 1), 2, "ws elem 1 = 2") + TkAssertEq&(DaGet&(da, 2), 3, "ws elem 2 = 3") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 18: Integer/float discrimination ============== *} + +PRINT "-- Test 18: Integer/float discrimination" +src$ = "[42, 3.14]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse int/float mix") +IF root& <> 0 THEN + da = root& + TkAssertEq%(DaType(da, 0), DaTypeLng, "42 type = lng") + TkAssertEq%(DaType(da, 1), DaTypeSng, "3.14 type = sng") + TkAssertEq&(DaGet&(da, 0), 42, "42 val = 42") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_parse_misc.b b/submods/json/test_parse_misc.b new file mode 100644 index 0000000..57a66d3 --- /dev/null +++ b/submods/json/test_parse_misc.b @@ -0,0 +1,352 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_parse_misc.b - Phase 3: Escapes, file input, error edge cases +** Tests: string escapes, JsParseFile, error conditions +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 3: Escapes, File Input, Errors ===" +PRINT + +TkInit + +STRING src$ SIZE 4096 +STRING errMsg$ SIZE 128 +STRING q$ SIZE 4 +q$ = CHR$(34) +STRING bsl$ SIZE 4 +bsl$ = CHR$(92) + +LONGINT root& +DECLARE CLASS Hashmap hm +DECLARE CLASS DynArray da +STRING got$ SIZE 256 +STRING exp$ SIZE 256 + +{* ============== Test 1: Escaped quotes ============== *} + +PRINT "-- Test 1: Escaped quotes in string" +' JSON: {"msg":"say \"hi\""} +src$ = "{" + q$ + "msg" + q$ + ":" + q$ + "say " + bsl$ + q$ + "hi" + bsl$ + q$ + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc quote") +IF root& <> 0 THEN + hm = root& + got$ = HmGet$(hm, "msg") + exp$ = "say " + q$ + "hi" + q$ + TkAssertEqStr(got$, exp$, "msg = say qhiq") + TkAssertEq&(LEN(got$), 8, "esc quote len = 8") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 2: Escaped backslash ============== *} + +PRINT "-- Test 2: Escaped backslash" +' JSON: {"p":"c:\\dir"} +src$ = "{" + q$ + "p" + q$ + ":" + q$ + "c:" + bsl$ + bsl$ + "dir" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc backslash") +IF root& <> 0 THEN + hm = root& + got$ = HmGet$(hm, "p") + exp$ = "c:" + bsl$ + "dir" + TkAssertEqStr(got$, exp$, "p = c bsl dir") + TkAssertEq&(LEN(got$), 6, "esc bsl len = 6") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 3: Escaped newline and tab ============== *} + +PRINT "-- Test 3: Escaped newline and tab" +' JSON: {"s":"a\nb\tc"} +src$ = "{" + q$ + "s" + q$ + ":" + q$ + "a" + bsl$ + "nb" + bsl$ + "tc" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc nl/tab") +IF root& <> 0 THEN + hm = root& + got$ = HmGet$(hm, "s") + exp$ = "a" + CHR$(10) + "b" + CHR$(9) + "c" + TkAssertEqStr(got$, exp$, "s = a LF b TAB c") + TkAssertEq&(LEN(got$), 5, "esc nl/tab len = 5") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 4: Escaped CR, BS, FF ============== *} + +PRINT "-- Test 4: Escaped CR, BS, FF" +' JSON: {"s":"x\ry\bz\fw"} +src$ = "{" + q$ + "s" + q$ + ":" + q$ + "x" + bsl$ + "r" +src$ = src$ + "y" + bsl$ + "b" + "z" + bsl$ + "f" + "w" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc cr/bs/ff") +IF root& <> 0 THEN + hm = root& + got$ = HmGet$(hm, "s") + TkAssertEq&(LEN(got$), 7, "esc cr/bs/ff len = 7") + ' Check specific bytes via PEEK + LONGINT addr& + addr& = SADD(got$) + TkAssertEq%(PEEK(addr&), 120, "byte 0 = x (120)") + TkAssertEq%(PEEK(addr& + 1), 13, "byte 1 = CR (13)") + TkAssertEq%(PEEK(addr& + 2), 121, "byte 2 = y (121)") + TkAssertEq%(PEEK(addr& + 3), 8, "byte 3 = BS (8)") + TkAssertEq%(PEEK(addr& + 4), 122, "byte 4 = z (122)") + TkAssertEq%(PEEK(addr& + 5), 12, "byte 5 = FF (12)") + TkAssertEq%(PEEK(addr& + 6), 119, "byte 6 = w (119)") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 5: Escaped slash ============== *} + +PRINT "-- Test 5: Escaped slash" +' JSON: {"u":"a\/b"} +src$ = "{" + q$ + "u" + q$ + ":" + q$ + "a" + bsl$ + "/b" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc slash") +IF root& <> 0 THEN + hm = root& + TkAssertEqStr(HmGet$(hm, "u"), "a/b", "u = a/b") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 6: Multiple escapes in one string ============== *} + +PRINT "-- Test 6: Multiple escapes" +' JSON: {"s":"a\"b\\c\/d"} +src$ = "{" + q$ + "s" + q$ + ":" + q$ +src$ = src$ + "a" + bsl$ + q$ + "b" + bsl$ + bsl$ + "c" + bsl$ + "/d" +src$ = src$ + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse multi esc") +IF root& <> 0 THEN + hm = root& + got$ = HmGet$(hm, "s") + exp$ = "a" + q$ + "b" + bsl$ + "c/d" + TkAssertEqStr(got$, exp$, "multi esc val") + TkAssertEq&(LEN(got$), 7, "multi esc len = 7") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 7: Empty string value ============== *} + +PRINT "-- Test 7: Empty string value" +src$ = "{" + q$ + "e" + q$ + ":" + q$ + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse empty str") +IF root& <> 0 THEN + hm = root& + TkAssertEqStr(HmGet$(hm, "e"), "", "e = empty") + TkAssertEq&(LEN(HmGet$(hm, "e")), 0, "empty len = 0") + TkAssertEq%(HmType(hm, "e"), HmTypeStr, "e type = str") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 8: Escaped quotes in array ============== *} + +PRINT "-- Test 8: Escaped quotes in array" +' JSON: ["say \"hi\"", "ok"] +src$ = "[" + q$ + "say " + bsl$ + q$ + "hi" + bsl$ + q$ + q$ + "," +src$ = src$ + q$ + "ok" + q$ + "]" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse arr esc quote") +IF root& <> 0 THEN + da = root& + TkAssertEq&(DaCount(da), 2, "arr esc count = 2") + got$ = DaGet$(da, 0) + exp$ = "say " + q$ + "hi" + q$ + TkAssertEqStr(got$, exp$, "arr[0] = say qhiq") + TkAssertEqStr(DaGet$(da, 1), "ok", "arr[1] = ok") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 9: JsParseFile - simple ============== *} + +PRINT "-- Test 9: JsParseFile simple" +' Write JSON to temp file +OPEN "O", #1, "T:test_json.tmp" +src$ = "{" + q$ + "nm" + q$ + ":" + q$ + "Bob" + q$ + "}" +PRINT #1, src$ +CLOSE #1 + +' Read back via JsParseFile +OPEN "I", #1, "T:test_json.tmp" +root& = JsParseFile(1) +CLOSE #1 + +TkAssertNeq&(root&, 0, "parsefile not null") +IF root& <> 0 THEN + TkAssertEq%(JsRootType, JsObject, "pf root = object") + hm = root& + TkAssertEqStr(HmGet$(hm, "nm"), "Bob", "pf nm = Bob") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 10: JsParseFile - multi-line ============== *} + +PRINT "-- Test 10: JsParseFile multi-line" +OPEN "O", #1, "T:test_json2.tmp" +PRINT #1, "{" +PRINT #1, " "; q$; "x"; q$; ": 10," +PRINT #1, " "; q$; "y"; q$; ": 20" +PRINT #1, "}" +CLOSE #1 + +OPEN "I", #1, "T:test_json2.tmp" +root& = JsParseFile(1) +CLOSE #1 + +TkAssertNeq&(root&, 0, "pf multiline ok") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmGet&(hm, "x"), 10, "pf x = 10") + TkAssertEq&(HmGet&(hm, "y"), 20, "pf y = 20") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 11: JsParseFile - array ============== *} + +PRINT "-- Test 11: JsParseFile array" +OPEN "O", #1, "T:test_json3.tmp" +src$ = "[1, 2, 3]" +PRINT #1, src$ +CLOSE #1 + +OPEN "I", #1, "T:test_json3.tmp" +root& = JsParseFile(1) +CLOSE #1 + +TkAssertNeq&(root&, 0, "pf array ok") +IF root& <> 0 THEN + TkAssertEq%(JsRootType, JsArray, "pf root = array") + da = root& + TkAssertEq&(DaCount(da), 3, "pf arr count = 3") + TkAssertEq&(DaGet&(da, 0), 1, "pf arr[0] = 1") + TkAssertEq&(DaGet&(da, 1), 2, "pf arr[1] = 2") + TkAssertEq&(DaGet&(da, 2), 3, "pf arr[2] = 3") + DaFree(da) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 12: Error - unexpected EOF ============== *} + +PRINT "-- Test 12: Error - unexpected EOF" +src$ = "{" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "unexp EOF -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "unexp EOF -> error msg") + +{* ============== Test 13: Error - invalid literal ============== *} + +PRINT "-- Test 13: Error - invalid literal" +src$ = "{" + q$ + "x" + q$ + ":tru}" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "bad literal -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "bad literal -> error msg") + +{* ============== Test 14: Error - missing close bracket ============== *} + +PRINT "-- Test 14: Error - missing close bracket" +src$ = "[1, 2, 3" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "no close -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "no close -> error msg") + +{* ============== Test 15: Error - empty input ============== *} + +PRINT "-- Test 15: Error - empty input" +src$ = "" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "empty -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "empty -> error msg") + +{* ============== Test 16: Whitespace with newlines and tabs ============== *} + +PRINT "-- Test 16: Whitespace with newlines and tabs" +src$ = "{" + CHR$(10) + CHR$(9) + q$ + "k" + q$ + " :" + CHR$(10) + " 42" + CHR$(10) + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse ws nl/tab") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmGet&(hm, "k"), 42, "ws nl/tab k = 42") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 17: Escaped key name ============== *} + +PRINT "-- Test 17: Escaped key name" +' JSON: {"a\"b":1} +src$ = "{" + q$ + "a" + bsl$ + q$ + "b" + q$ + ":1}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse esc key") +IF root& <> 0 THEN + hm = root& + exp$ = "a" + q$ + "b" + TkAssertEq&(HmGet&(hm, exp$), 1, "esc key val = 1") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_parse_obj.b b/submods/json/test_parse_obj.b new file mode 100644 index 0000000..efe88a2 --- /dev/null +++ b/submods/json/test_parse_obj.b @@ -0,0 +1,236 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_parse_obj.b - Phase 1: Parse JSON objects with scalar values +** Tests: JsParse, JsError$, JsRootType +** Value types: string, integer, boolean, null +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 1: Parse JSON Objects ===" +PRINT + +TkInit + +STRING src$ SIZE 4096 +STRING errMsg$ SIZE 128 +STRING q$ SIZE 4 +q$ = CHR$(34) + +LONGINT root& +DECLARE CLASS Hashmap hm + +{* ============== Test 1: Empty object ============== *} + +PRINT "-- Test 1: Empty object" +src$ = "{}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse {} not null") +IF root& <> 0 THEN + TkAssertEq%(JsRootType, JsObject, "root type = object") + hm = root& + TkAssertEq&(HmCount(hm), 0, "empty count = 0") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 2: Single string ============== *} + +PRINT "-- Test 2: Single string" +src$ = "{" + q$ + "city" + q$ + ":" + q$ + "Berlin" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse single str") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 1, "count = 1") + TkAssertEqStr(HmGet$(hm, "city"), "Berlin", "city = Berlin") + TkAssertEq%(HmType(hm, "city"), HmTypeStr, "city type = str") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 3: Multiple strings ============== *} + +PRINT "-- Test 3: Multiple strings" +src$ = "{" + q$ + "a" + q$ + ":" + q$ + "one" + q$ + "," +src$ = src$ + q$ + "b" + q$ + ":" + q$ + "two" + q$ + "}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse multi str") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 2, "count = 2") + TkAssertEqStr(HmGet$(hm, "a"), "one", "a = one") + TkAssertEqStr(HmGet$(hm, "b"), "two", "b = two") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 4: Integer values ============== *} + +PRINT "-- Test 4: Integer values" +src$ = "{" + q$ + "pos" + q$ + ":42," +src$ = src$ + q$ + "neg" + q$ + ":-7," +src$ = src$ + q$ + "zero" + q$ + ":0}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse integers") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 3, "count = 3") + TkAssertEq&(HmGet&(hm, "pos"), 42, "pos = 42") + TkAssertEq&(HmGet&(hm, "neg"), -7, "neg = -7") + TkAssertEq&(HmGet&(hm, "zero"), 0, "zero = 0") + TkAssertEq%(HmType(hm, "pos"), HmTypeLng, "pos type = lng") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 5: Boolean values ============== *} + +PRINT "-- Test 5: Boolean values" +src$ = "{" + q$ + "on" + q$ + ":true," +src$ = src$ + q$ + "off" + q$ + ":false}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse booleans") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 2, "count = 2") + TkAssertEq%(HmType(hm, "on"), HmTypeBool, "on type = bool") + TkAssertEq%(HmType(hm, "off"), HmTypeBool, "off type = bool") + TkAssertEq&(HmGet&(hm, "on"), 1, "on = 1 (true)") + TkAssertEq&(HmGet&(hm, "off"), 0, "off = 0 (false)") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 6: Null value ============== *} + +PRINT "-- Test 6: Null value" +src$ = "{" + q$ + "gone" + q$ + ":null}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse null") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 1, "count = 1") + TkAssertEq%(HmType(hm, "gone"), HmTypeNull, "gone type = null") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 7: Mixed types ============== *} + +PRINT "-- Test 7: Mixed types" +src$ = "{" + q$ + "nm" + q$ + ":" + q$ + "Alice" + q$ + "," +src$ = src$ + q$ + "ag" + q$ + ":30," +src$ = src$ + q$ + "ok" + q$ + ":true," +src$ = src$ + q$ + "rm" + q$ + ":null}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse mixed") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmCount(hm), 4, "count = 4") + TkAssertEqStr(HmGet$(hm, "nm"), "Alice", "nm = Alice") + TkAssertEq&(HmGet&(hm, "ag"), 30, "ag = 30") + TkAssertEq%(HmType(hm, "ok"), HmTypeBool, "ok type = bool") + TkAssertEq%(HmType(hm, "rm"), HmTypeNull, "rm type = null") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 8: Whitespace tolerance ============== *} + +PRINT "-- Test 8: Whitespace tolerance" +src$ = " { " + q$ + "x" + q$ + " : " + q$ + "y" + q$ + " } " +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse with whitespace") +IF root& <> 0 THEN + hm = root& + TkAssertEqStr(HmGet$(hm, "x"), "y", "x = y") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Test 9: Error - missing colon ============== *} + +PRINT "-- Test 9: Error - missing colon" +src$ = "{" + q$ + "x" + q$ + " " + q$ + "y" + q$ + "}" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "missing colon -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "missing colon -> error msg") + +{* ============== Test 10: Error - unterminated string ============== *} + +PRINT "-- Test 10: Error - unterminated string" +src$ = "{" + q$ + "x" + q$ + ":" + q$ + "hello}" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "unterm string -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "unterm string -> error msg") + +{* ============== Test 11: Error - trailing comma ============== *} + +PRINT "-- Test 11: Error - trailing comma" +src$ = "{" + q$ + "x" + q$ + ":1,}" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "trailing comma -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "trailing comma -> error msg") + +{* ============== Test 12: Error - invalid input ============== *} + +PRINT "-- Test 12: Error - invalid input" +src$ = "hello" +root& = JsParse(src$) +TkAssertEq&(root&, 0, "invalid input -> null") +errMsg$ = JsError$ +TkAssertTrue(LEN(errMsg$) > 0, "invalid input -> error msg") + +{* ============== Test 13: Large integer ============== *} + +PRINT "-- Test 13: Large integer" +src$ = "{" + q$ + "big" + q$ + ":1000000}" +root& = JsParse(src$) +TkAssertNeq&(root&, 0, "parse large int") +IF root& <> 0 THEN + hm = root& + TkAssertEq&(HmGet&(hm, "big"), 1000000, "big = 1000000") + HmFree(hm) + FREE root& +ELSE + errMsg$ = JsError$ + PRINT " Error: "; errMsg$ +END IF + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_roundtrip.b b/submods/json/test_roundtrip.b new file mode 100644 index 0000000..4f5f47a --- /dev/null +++ b/submods/json/test_roundtrip.b @@ -0,0 +1,350 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/json/json.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_roundtrip.b - Phase 5: Pretty printer + round-trip +** Tests: JsWriteFmt, parse->generate->compare +** Note: JsFree not yet implemented (Phase 6), parsed trees are leaked. +*} + +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 5: Pretty Printer + Round-Trip ===" +PRINT + +TkInit + +STRING result$ SIZE 4096 +STRING expected$ SIZE 4096 +STRING src$ SIZE 4096 +STRING ln$ SIZE 512 +STRING q$ SIZE 4 +q$ = CHR$(34) + +LONGINT root&, root2& +DECLARE CLASS Hashmap hm +DECLARE CLASS DynArray da + +{* ============== Test 1: Fmt empty object ============== *} + +PRINT "-- Test 1: Fmt empty object" +HmMake(hm, HM_SMALL) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(hm, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +CLOSE #1 +TkAssertEqStr(ln$, "{}", "fmt empty obj") +HmFree(hm) + +{* ============== Test 2: Fmt empty array ============== *} + +PRINT "-- Test 2: Fmt empty array" +DaMake(da, DA_SMALL) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(da, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +CLOSE #1 +TkAssertEqStr(ln$, "[]", "fmt empty arr") +DaFree(da) + +{* ============== Test 3: Fmt single-key object ============== *} + +PRINT "-- Test 3: Fmt single-key object" +HmMake(hm, HM_SMALL) +HmPut$(hm, "city", "Berlin") +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(hm, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "{", "fmt obj L1 {") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "city" + q$ + ": " + q$ + "Berlin" + q$ +TkAssertEqStr(ln$, expected$, "fmt obj L2 kv") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "}", "fmt obj L3 }") +CLOSE #1 +HmFree(hm) + +{* ============== Test 4: Fmt integer array ============== *} + +PRINT "-- Test 4: Fmt integer array" +DaMake(da, DA_SMALL) +DaAppend&(da, 10) +DaAppend&(da, 20) +DaAppend&(da, 30) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(da, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "[", "fmt arr L1 [") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " 10,", "fmt arr L2 10,") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " 20,", "fmt arr L3 20,") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " 30", "fmt arr L4 30") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "]", "fmt arr L5 ]") +CLOSE #1 +DaFree(da) + +{* ============== Test 5: Fmt nested array in object ============== *} + +PRINT "-- Test 5: Fmt nested array in object" +DaMake(da, DA_SMALL) +DaAppend&(da, 1) +DaAppend&(da, 2) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "arr", da) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(hm, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "{", "fmt nest L1 {") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "arr" + q$ + ": [" +TkAssertEqStr(ln$, expected$, "fmt nest L2 arr:[") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " 1,", "fmt nest L3 1,") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " 2", "fmt nest L4 2") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " ]", "fmt nest L5 ]") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "}", "fmt nest L6 }") +CLOSE #1 +DaFree(da) +HmFree(hm) + +{* ============== Test 6: Fmt nested object in object ============== *} + +PRINT "-- Test 6: Fmt nested object in object" +DECLARE CLASS Hashmap inner +HmMake(inner, HM_SMALL) +HmPut&(inner, "x", 99) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "obj", inner) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(hm, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "{", "fmt nobj L1 {") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "obj" + q$ + ": {" +TkAssertEqStr(ln$, expected$, "fmt nobj L2 obj:{") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "x" + q$ + ": 99" +TkAssertEqStr(ln$, expected$, "fmt nobj L3 x:99") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " }", "fmt nobj L4 }") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "}", "fmt nobj L5 }") +CLOSE #1 +HmFree(inner) +HmFree(hm) + +{* ============== Test 7: Fmt bool/null array ============== *} + +PRINT "-- Test 7: Fmt bool/null array" +DaMake(da, DA_SMALL) +DaAppendBool(da, -1) +DaAppendNull(da) +DaAppendBool(da, 0) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(da, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "[", "fmt bnl L1") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " true,", "fmt bnl L2") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " null,", "fmt bnl L3") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " false", "fmt bnl L4") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "]", "fmt bnl L5") +CLOSE #1 +DaFree(da) + +{* ============== Test 8: Compact round-trip int array ============== *} + +PRINT "-- Test 8: Compact round-trip int array" +src$ = "[1,-2,0,999]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt intarr parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt intarr match") + +{* ============== Test 9: Compact round-trip string array ============== *} + +PRINT "-- Test 9: Compact round-trip string array" +src$ = "[" + q$ + "aaa" + q$ + "," + q$ + "bbb" + q$ + "]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt strarr parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt strarr match") + +{* ============== Test 10: Compact round-trip mixed array ============== *} + +PRINT "-- Test 10: Compact round-trip mixed array" +src$ = "[42," + q$ + "hi" + q$ + ",true,false,null]" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt mixarr parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt mixarr match") + +{* ============== Test 11: Compact round-trip single-key obj ============== *} + +PRINT "-- Test 11: Compact round-trip single-key object" +src$ = "{" + q$ + "n" + q$ + ":42}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt 1key parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt 1key match") + +{* ============== Test 12: Compact round-trip string escapes ============== *} + +PRINT "-- Test 12: Compact round-trip string escapes" +' Input: {"k":"a\"b\\c"} +src$ = "{" + q$ + "k" + q$ + ":" + q$ + "a\" + q$ + "b\\c" + q$ + "}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt esc parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt esc match") + +{* ============== Test 13: Compact round-trip nested ============== *} + +PRINT "-- Test 13: Compact round-trip nested" +' Single-key objects preserve order -> string comparison works +src$ = "{" + q$ + "a" + q$ + ":[1,2]}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "rt nest parse") +result$ = JsToStr$(root&) +TkAssertEqStr(result$, src$, "rt nest match") + +{* ============== Test 14: Fmt round-trip (format then parse back) ============== *} + +PRINT "-- Test 14: Fmt round-trip array" +DaMake(da, DA_SMALL) +DaAppend$(da, "hello") +DaAppend&(da, 42) +DaAppendBool(da, -1) +OPEN "O", #1, "T:jsfmt_rt.json" +JsWriteFmt(da, 1) +CLOSE #1 +DaFree(da) + +' Parse the formatted output back +OPEN "I", #1, "T:jsfmt_rt.json" +root& = JsParseFile(1) +CLOSE #1 +TkAssertTrue(root& <> 0, "fmt rt parse ok") + +DECLARE CLASS DynArray da2 +da2 = root& +TkAssertEq&(DaCount(da2), 3, "fmt rt count") +TkAssertEqStr(DaGet$(da2, 0), "hello", "fmt rt str") +TkAssertEq&(DaGet&(da2, 1), 42, "fmt rt int") +TkAssertEq&(DaGet&(da2, 2), 1, "fmt rt bool") + +{* ============== Test 15: Fmt round-trip object ============== *} + +PRINT "-- Test 15: Fmt round-trip object" +HmMake(hm, HM_SMALL) +HmPut$(hm, "msg", "ok") +HmPut&(hm, "code", 200) +HmPutBool(hm, "flag", 0) +HmPutNull(hm, "nil") +OPEN "O", #1, "T:jsfmt_rt.json" +JsWriteFmt(hm, 1) +CLOSE #1 +HmFree(hm) + +' Parse back +OPEN "I", #1, "T:jsfmt_rt.json" +root& = JsParseFile(1) +CLOSE #1 +TkAssertTrue(root& <> 0, "fmt rt obj parse") + +DECLARE CLASS Hashmap doc +doc = root& +TkAssertEqStr(HmGet$(doc, "msg"), "ok", "fmt rt obj str") +TkAssertEq&(HmGet&(doc, "code"), 200, "fmt rt obj int") +TkAssertTrue(HmHas(doc, "flag"), "fmt rt obj has flag") +TkAssertEq%(HmType(doc, "nil"), HmTypeNull, "fmt rt obj null") + +{* ============== Test 16: Type preservation after round-trip ============== *} + +PRINT "-- Test 16: Type preservation" +src$ = "{" + q$ + "i" + q$ + ":7," + q$ + "b" + q$ + ":true," +src$ = src$ + q$ + "n" + q$ + ":null}" +root& = JsParse(src$) +TkAssertTrue(root& <> 0, "tp parse ok") + +doc = root& +TkAssertEq%(HmType(doc, "i"), HmTypeLng, "tp int type") +TkAssertEq%(HmType(doc, "b"), HmTypeBool, "tp bool type") +TkAssertEq%(HmType(doc, "n"), HmTypeNull, "tp null type") + +' Generate and parse again +result$ = JsToStr$(root&) +root2& = JsParse(result$) +TkAssertTrue(root2& <> 0, "tp reparse ok") +doc = root2& +TkAssertEq%(HmType(doc, "i"), HmTypeLng, "tp2 int type") +TkAssertEq&(HmGet&(doc, "i"), 7, "tp2 int val") +TkAssertEq%(HmType(doc, "b"), HmTypeBool, "tp2 bool type") +TkAssertEq%(HmType(doc, "n"), HmTypeNull, "tp2 null type") + +{* ============== Test 17: Deep nesting pretty-print ============== *} + +PRINT "-- Test 17: Deep nesting pretty-print" +' Build: {"d":[{"v":7}]} +DECLARE CLASS Hashmap deep +HmMake(deep, HM_SMALL) +HmPut&(deep, "v", 7) +DaMake(da, DA_SMALL) +DaAppendRef(da, deep) +HmMake(hm, HM_SMALL) +HmPutRef(hm, "d", da) +OPEN "O", #1, "T:jsfmt.txt" +JsWriteFmt(hm, 1) +CLOSE #1 +OPEN "I", #1, "T:jsfmt.txt" +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "{", "fmt deep L1") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "d" + q$ + ": [" +TkAssertEqStr(ln$, expected$, "fmt deep L2") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " {", "fmt deep L3") +LINE INPUT #1, ln$ +expected$ = " " + q$ + "v" + q$ + ": 7" +TkAssertEqStr(ln$, expected$, "fmt deep L4") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " }", "fmt deep L5") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, " ]", "fmt deep L6") +LINE INPUT #1, ln$ +TkAssertEqStr(ln$, "}", "fmt deep L7") +CLOSE #1 +HmFree(deep) +DaFree(da) +HmFree(hm) + +{* ============== Summary ============== *} +TkSummary diff --git a/submods/json/test_typecase.b b/submods/json/test_typecase.b new file mode 100644 index 0000000..d021ddf --- /dev/null +++ b/submods/json/test_typecase.b @@ -0,0 +1,168 @@ +REM #using ace:submods/hashmap/hashmap.o +REM #using ace:submods/dynarray/dynarray.o +REM #using ace:submods/testkit/testkit.o + +{* +** test_typecase.b - Phase 0: Verify CLASS descriptor stamping +** Tests that ALLOC'd Hashmap and DynArray instances (from builders) +** have proper descriptors for TYPECASE discrimination. +*} + +#include +#include +#include + +{* ============== Test Suite ============== *} + +PRINT "=== Phase 0: TYPECASE Descriptor Stamping ===" +PRINT + +TkInit + +LONGINT hmAddr&, daAddr& + +{* ============== Test 1: TYPECASE on builder Hashmap ============== *} + +PRINT "-- Test 1: TYPECASE on builder Hashmap" +HmNew(HM_SMALL) + HmAdd$("key", "val") +hmAddr& = HmEnd + +DECLARE CLASS Hashmap hm +hm = hmAddr& + +SHORTINT matched% +matched% = 0 + +TYPECASE hm + CASE Hashmap + matched% = 1 + CASE DynArray + matched% = 2 + CASE ELSE + matched% = -1 +END TYPECASE + +TkAssertEq%(matched%, 1, "builder Hashmap matches CASE Hashmap") + +' Verify it still works as a Hashmap +TkAssertEqStr(HmGet$(hm, "key"), "val", "Hashmap data intact") + +HmFree(hm) +FREE hmAddr& + +{* ============== Test 2: TYPECASE on builder DynArray ============== *} + +PRINT "-- Test 2: TYPECASE on builder DynArray" +DaNew(DA_SMALL) + DaAdd$("hello") +daAddr& = DaEnd + +DECLARE CLASS DynArray da +da = daAddr& + +matched% = 0 + +TYPECASE da + CASE Hashmap + matched% = 1 + CASE DynArray + matched% = 2 + CASE ELSE + matched% = -1 +END TYPECASE + +TkAssertEq%(matched%, 2, "builder DynArray matches CASE DynArray") + +' Verify it still works as a DynArray +TkAssertEqStr(DaGet$(da, 0), "hello", "DynArray data intact") + +DaFree(da) +FREE daAddr& + +{* ============== Test 3: Discriminate via common variable ============== *} + +PRINT "-- Test 3: Discriminate Hashmap vs DynArray" +HmNew(HM_SMALL) + HmAdd&("num", 42) +hmAddr& = HmEnd + +DaNew(DA_SMALL) + DaAdd&(99) +daAddr& = DaEnd + +' Use Hashmap variable but assign DynArray address +DECLARE CLASS Hashmap probe +SHORTINT result% + +' Test with Hashmap address +probe = hmAddr& +result% = 0 +TYPECASE probe + CASE Hashmap + result% = 1 + CASE DynArray + result% = 2 + CASE ELSE + result% = -1 +END TYPECASE +TkAssertEq%(result%, 1, "hmAddr matches Hashmap") + +' Test with DynArray address +probe = daAddr& +result% = 0 +TYPECASE probe + CASE Hashmap + result% = 1 + CASE DynArray + result% = 2 + CASE ELSE + result% = -1 +END TYPECASE +TkAssertEq%(result%, 2, "daAddr matches DynArray") + +' Cleanup +hm = hmAddr& +HmFree(hm) +FREE hmAddr& + +da = daAddr& +DaFree(da) +FREE daAddr& + +{* ============== Test 4: BSS-backed instances still work ============== *} + +PRINT "-- Test 4: BSS-backed CLASS instances" +DECLARE CLASS Hashmap bssHm +DECLARE CLASS DynArray bssDa + +HmMake(bssHm, HM_SMALL) +DaMake(bssDa, DA_SMALL) + +' TYPECASE on BSS Hashmap +probe = bssHm +result% = 0 +TYPECASE probe + CASE Hashmap + result% = 1 + CASE DynArray + result% = 2 +END TYPECASE +TkAssertEq%(result%, 1, "BSS Hashmap matches Hashmap") + +' TYPECASE on BSS DynArray +probe = bssDa +result% = 0 +TYPECASE probe + CASE Hashmap + result% = 1 + CASE DynArray + result% = 2 +END TYPECASE +TkAssertEq%(result%, 2, "BSS DynArray matches DynArray") + +HmFree(bssHm) +DaFree(bssDa) + +{* ============== Summary ============== *} +TkSummary diff --git a/verify/scripts/otherthenamiga/call-on-ustartup b/verify/scripts/otherthenamiga/call-on-ustartup index 139597f..f3a26a7 100644 --- a/verify/scripts/otherthenamiga/call-on-ustartup +++ b/verify/scripts/otherthenamiga/call-on-ustartup @@ -1,2 +1,18 @@ +; Phase 6: Compile json module + test JsFree and convenience +cd ace:submods/json +; Recompile json module (JsFree, JsMakeObj, JsMakeArr added) +execute make >ace:build-output.txt + +; Compile and run Phase 6 test +execute ACE:bin/bas test_free >>ace:build-output.txt +test_free >>ace:build-output.txt + +; Regression: run Phase 5 test +execute ACE:bin/bas test_roundtrip >>ace:build-output.txt +test_roundtrip >>ace:build-output.txt + +; Regression: run Phase 4 test +execute ACE:bin/bas test_gen >>ace:build-output.txt +test_gen >>ace:build-output.txt From 8cd5897a60ecc8807d7ea475ccf9c8f1fd6c73e3 Mon Sep 17 00:00:00 2001 From: Manfred Bergmann Date: Wed, 25 Feb 2026 22:21:08 +0100 Subject: [PATCH 3/3] Merge _JsWriteNode and _JsWriteNodeFmt into single unified writer Use lvl% = -1 for compact mode, lvl% >= 0 for formatted mode. Eliminates 74 lines of duplicated value-dispatch code. Co-Authored-By: Claude Opus 4.6 --- submods/json/json.b | 120 ++++-------------- .../scripts/otherthenamiga/call-on-ustartup | 8 +- 2 files changed, 26 insertions(+), 102 deletions(-) diff --git a/submods/json/json.b b/submods/json/json.b index 08941e7..dfd4d8e 100644 --- a/submods/json/json.b +++ b/submods/json/json.b @@ -720,10 +720,12 @@ SUB _JsWriteStr(s$, SHORTINT ch%) END SUB {* ============== Generator: Recursive node writer ============== *} +' lvl% = -1: compact mode (no whitespace) +' lvl% >= 0: formatted mode (2-space indent at that level) -SUB _JsWriteNode(ADDRESS addr&, SHORTINT ch%) - LONGINT savedAddr&, i&, cnt& - SHORTINT first%, typ% +SUB _JsWriteNode(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) + LONGINT savedAddr&, i&, cnt&, nextLvl& + SHORTINT first%, typ%, fmt%, pad% SINGLE fVal! DECLARE CLASS Hashmap tcItem @@ -731,6 +733,8 @@ SUB _JsWriteNode(ADDRESS addr&, SHORTINT ch%) savedAddr& = addr& tcItem = addr& + fmt% = (lvl% >= 0) + IF fmt% THEN nextLvl& = lvl% + 1 ELSE nextLvl& = -1 TYPECASE tcItem CASE Hashmap @@ -739,92 +743,14 @@ SUB _JsWriteNode(ADDRESS addr&, SHORTINT ch%) first% = -1 WHILE HmIterNext(tcItem) IF NOT first% THEN PRINT #ch%, ","; - first% = 0 - _JsWriteStr(HmIterKey$(tcItem), ch%) - PRINT #ch%, ":"; - - typ% = HmIterType(tcItem) - IF typ% = HmTypeStr THEN - _JsWriteStr(HmIterVal$(tcItem), ch%) - ELSEIF typ% = HmTypeLng THEN - PRINT #ch%, LTRIM$(STR$(HmIterVal&(tcItem))); - ELSEIF typ% = HmTypeSng THEN - fVal! = HmIterVal!(tcItem) - PRINT #ch%, LTRIM$(STR$(fVal!)); - ELSEIF typ% = HmTypeBool THEN - IF HmIterVal&(tcItem) THEN - PRINT #ch%, "true"; - ELSE - PRINT #ch%, "false"; - END IF - ELSEIF typ% = HmTypeNull THEN - PRINT #ch%, "null"; - ELSEIF typ% = HmTypeRef THEN - _JsWriteNode(HmIterVal&(tcItem), ch%) - tcItem = savedAddr& + IF fmt% THEN + PRINT #ch%, CHR$(10); + pad% = (lvl% + 1) * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); END IF - WEND - PRINT #ch%, "}"; - - CASE DynArray - cnt& = DaCount(tcItem) - PRINT #ch%, "["; - FOR i& = 0 TO cnt& - 1 - IF i& > 0 THEN PRINT #ch%, ","; - - typ% = DaType(tcItem, i&) - IF typ% = DaTypeStr THEN - _JsWriteStr(DaGet$(tcItem, i&), ch%) - ELSEIF typ% = DaTypeLng THEN - PRINT #ch%, LTRIM$(STR$(DaGet&(tcItem, i&))); - ELSEIF typ% = DaTypeSng THEN - fVal! = DaGet!(tcItem, i&) - PRINT #ch%, LTRIM$(STR$(fVal!)); - ELSEIF typ% = DaTypeBool THEN - IF DaGet&(tcItem, i&) THEN - PRINT #ch%, "true"; - ELSE - PRINT #ch%, "false"; - END IF - ELSEIF typ% = DaTypeNull THEN - PRINT #ch%, "null"; - ELSEIF typ% = DaTypeRef THEN - _JsWriteNode(DaGet&(tcItem, i&), ch%) - tcItem = savedAddr& - END IF - NEXT - PRINT #ch%, "]"; - END TYPECASE -END SUB - -{* ============== Generator: Formatted (pretty-print) node writer ============== *} - -SUB _JsWriteNodeFmt(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) - LONGINT savedAddr&, i&, cnt& - SHORTINT first%, typ%, pad% - SINGLE fVal! - DECLARE CLASS Hashmap tcItem - - IF addr& = 0 THEN EXIT SUB - - savedAddr& = addr& - tcItem = addr& - - TYPECASE tcItem - CASE Hashmap - PRINT #ch%, "{"; - HmIterReset(tcItem) - first% = -1 - WHILE HmIterNext(tcItem) - IF NOT first% THEN - PRINT #ch%, ","; - END IF - PRINT #ch%, CHR$(10); first% = 0 - pad% = (lvl% + 1) * 2 - IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); _JsWriteStr(HmIterKey$(tcItem), ch%) - PRINT #ch%, ": "; + IF fmt% THEN PRINT #ch%, ": "; ELSE PRINT #ch%, ":"; typ% = HmIterType(tcItem) IF typ% = HmTypeStr THEN @@ -843,11 +769,11 @@ SUB _JsWriteNodeFmt(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) ELSEIF typ% = HmTypeNull THEN PRINT #ch%, "null"; ELSEIF typ% = HmTypeRef THEN - _JsWriteNodeFmt(HmIterVal&(tcItem), ch%, lvl% + 1) + _JsWriteNode(HmIterVal&(tcItem), ch%, nextLvl&) tcItem = savedAddr& END IF WEND - IF NOT first% THEN + IF fmt% AND NOT first% THEN PRINT #ch%, CHR$(10); pad% = lvl% * 2 IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); @@ -858,12 +784,12 @@ SUB _JsWriteNodeFmt(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) cnt& = DaCount(tcItem) PRINT #ch%, "["; FOR i& = 0 TO cnt& - 1 - IF i& > 0 THEN - PRINT #ch%, ","; + IF i& > 0 THEN PRINT #ch%, ","; + IF fmt% THEN + PRINT #ch%, CHR$(10); + pad% = (lvl% + 1) * 2 + IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); END IF - PRINT #ch%, CHR$(10); - pad% = (lvl% + 1) * 2 - IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); typ% = DaType(tcItem, i&) IF typ% = DaTypeStr THEN @@ -882,11 +808,11 @@ SUB _JsWriteNodeFmt(ADDRESS addr&, SHORTINT ch%, SHORTINT lvl%) ELSEIF typ% = DaTypeNull THEN PRINT #ch%, "null"; ELSEIF typ% = DaTypeRef THEN - _JsWriteNodeFmt(DaGet&(tcItem, i&), ch%, lvl% + 1) + _JsWriteNode(DaGet&(tcItem, i&), ch%, nextLvl&) tcItem = savedAddr& END IF NEXT - IF cnt& > 0 THEN + IF fmt% AND cnt& > 0 THEN PRINT #ch%, CHR$(10); pad% = lvl% * 2 IF pad% > 0 THEN PRINT #ch%, SPACE$(pad%); @@ -899,12 +825,12 @@ END SUB SUB JsWrite(ADDRESS root&, SHORTINT ch%) EXTERNAL IF root& = 0 THEN EXIT SUB - _JsWriteNode(root&, ch%) + _JsWriteNode(root&, ch%, -1) END SUB SUB JsWriteFmt(ADDRESS root&, SHORTINT ch%) EXTERNAL IF root& = 0 THEN EXIT SUB - _JsWriteNodeFmt(root&, ch%, 0) + _JsWriteNode(root&, ch%, 0) END SUB SUB STRING JsToStr$(ADDRESS root&) EXTERNAL diff --git a/verify/scripts/otherthenamiga/call-on-ustartup b/verify/scripts/otherthenamiga/call-on-ustartup index f3a26a7..1431e92 100644 --- a/verify/scripts/otherthenamiga/call-on-ustartup +++ b/verify/scripts/otherthenamiga/call-on-ustartup @@ -1,18 +1,16 @@ -; Phase 6: Compile json module + test JsFree and convenience +; Verify merged _JsWriteNode (compact + formatted) cd ace:submods/json -; Recompile json module (JsFree, JsMakeObj, JsMakeArr added) +; Recompile json module execute make >ace:build-output.txt -; Compile and run Phase 6 test +; Run all test suites execute ACE:bin/bas test_free >>ace:build-output.txt test_free >>ace:build-output.txt -; Regression: run Phase 5 test execute ACE:bin/bas test_roundtrip >>ace:build-output.txt test_roundtrip >>ace:build-output.txt -; Regression: run Phase 4 test execute ACE:bin/bas test_gen >>ace:build-output.txt test_gen >>ace:build-output.txt