@@ -309,6 +309,141 @@ def check_junk_comments(repo_root: Path) -> list[str]:
309309 return errors
310310
311311
312+ def check_allocate_deallocate_pairing (repo_root : Path ) -> list [str ]:
313+ """Flag @:ALLOCATE'd array names with no matching @:DEALLOCATE anywhere in src/.
314+
315+ Every @:ALLOCATE must have a matching @:DEALLOCATE so that GPU device
316+ memory is properly released alongside host memory.
317+ """
318+ errors : list [str ] = []
319+ src_dir = repo_root / SRC_DIR
320+
321+ # Known program-lifetime allocations with no finalize subroutine
322+ PROGRAM_LIFETIME_EXEMPTIONS = {
323+ # m_helper
324+ "weight" ,
325+ "pb0" ,
326+ "Re_trans_T" ,
327+ # m_model
328+ "gpu_ntrs" ,
329+ "gpu_trs_v" ,
330+ "gpu_trs_n" ,
331+ "gpu_boundary_edge_count" ,
332+ "gpu_total_vertices" ,
333+ "gpu_boundary_v" ,
334+ # m_variables_conversion
335+ "gs_min" ,
336+ "pi_infs" ,
337+ "ps_inf" ,
338+ "cvs" ,
339+ "qvs" ,
340+ "qvps" ,
341+ "Gs_vc" ,
342+ # m_start_up post_process
343+ "data_in" ,
344+ "data_out" ,
345+ "data_cmplx" ,
346+ "data_cmplx_y" ,
347+ "data_cmplx_z" ,
348+ "En_real" ,
349+ "En" ,
350+ # m_acoustic_src
351+ "loc_acoustic" ,
352+ "mass_src" ,
353+ "mom_src" ,
354+ "E_src" ,
355+ "source_spatials_num_points" ,
356+ "source_spatials" ,
357+ # m_bubbles_EE
358+ "bub_adv_src" ,
359+ "bub_r_src" ,
360+ "bub_v_src" ,
361+ "bub_p_src" ,
362+ "bub_m_src" ,
363+ "divu" ,
364+ "ms" ,
365+ "ps" ,
366+ "rs" ,
367+ "vs" ,
368+ # m_qbmm
369+ "bubmoms" ,
370+ "momrhs" ,
371+ # m_cbc
372+ "F_src_rsx_vf" ,
373+ "flux_src_rsx_vf_l" ,
374+ "F_src_rsy_vf" ,
375+ "flux_src_rsy_vf_l" ,
376+ "F_src_rsz_vf" ,
377+ "flux_src_rsz_vf_l" ,
378+ "pres_in" ,
379+ "Del_in" ,
380+ "alpha_rho_in" ,
381+ # m_data_output
382+ "Rc_sf" ,
383+ # m_fftw
384+ "data_cmplx_gpu" ,
385+ "data_fltr_cmplx_gpu" ,
386+ # m_global_parameters
387+ "qbmm_idx" ,
388+ "x_cc" ,
389+ "dx" ,
390+ "y_cc" ,
391+ "dy" ,
392+ "z_cc" ,
393+ "dz" ,
394+ # m_hypoelastic
395+ "dw_dx_hypo" ,
396+ # m_igr
397+ "coeff_R" ,
398+ # m_rhs
399+ "qR_rsx_vf" ,
400+ "dqR_rsx_vf" ,
401+ # m_surface_tension
402+ "gR_x" ,
403+ # m_time_steppers
404+ "q_prim_ts2" ,
405+ # m_weno
406+ "poly_coef_cbR_x" ,
407+ "d_cbR_x" ,
408+ "poly_coef_cbR_y" ,
409+ "d_cbR_y" ,
410+ "poly_coef_cbR_z" ,
411+ "d_cbR_z" ,
412+ "divu%sf" ,
413+ "qbmm_idx%ps" ,
414+ }
415+
416+ allocate_re = re .compile (r"@:ALLOCATE\((\w[\w%]*)" )
417+ deallocate_re = re .compile (r"@:DEALLOCATE\((\w[\w%]*)" )
418+
419+ # Collect all deallocated names across all files
420+ deallocated : set [str ] = set ()
421+ for src in _fortran_fpp_files (src_dir ):
422+ text = src .read_text (encoding = "utf-8" )
423+ for match in deallocate_re .finditer (text ):
424+ deallocated .add (match .group (1 ))
425+
426+ # Check each allocation against the global deallocated set
427+ for src in _fortran_fpp_files (src_dir ):
428+ lines = src .read_text (encoding = "utf-8" ).splitlines ()
429+ rel = src .relative_to (repo_root )
430+ for i , line in enumerate (lines ):
431+ stripped = line .strip ()
432+ if _is_comment_or_blank (stripped ):
433+ continue
434+ match = allocate_re .search (stripped )
435+ if match :
436+ name = match .group (1 )
437+ if name not in deallocated and name not in PROGRAM_LIFETIME_EXEMPTIONS :
438+ errors .append (
439+ f" { rel } :{ i + 1 } @:ALLOCATE({ name } ...) has no matching "
440+ f"@:DEALLOCATE anywhere in src/. Fix: add @:DEALLOCATE in "
441+ f"the module's s_finalize_* subroutine or annotate as program-lifetime"
442+ )
443+
444+ return errors
445+
446+
312447def main ():
313448 repo_root = Path (__file__ ).resolve ().parents [2 ]
314449
@@ -321,6 +456,7 @@ def main():
321456 all_errors .extend (check_fypp_list_duplicates (repo_root ))
322457 all_errors .extend (check_duplicate_lines (repo_root ))
323458 all_errors .extend (check_hardcoded_byte_size (repo_root ))
459+ all_errors .extend (check_allocate_deallocate_pairing (repo_root ))
324460
325461 if all_errors :
326462 print ("Source lint failed:" )
0 commit comments