roup/
c_api.rs

1//! # Minimal Unsafe C API for OpenMP Parser
2//!
3//! This module provides a direct pointer-based C FFI with minimal unsafe code.
4//!
5//! ## Design Philosophy: Direct Pointers vs Handles
6//!
7//! We use **raw C pointers** to Rust structs instead of opaque handles for:
8//! - **Idiomatic C API**: Standard malloc/free pattern familiar to C programmers
9//! - **Simple memory model**: No global registry, no handle bookkeeping
10//! - **Easy integration**: Works naturally with C/C++ code
11//! - **Minimal code**: 632 lines vs 4000+ lines of handle management
12//!
13//! ## Safety Analysis: 18 Unsafe Blocks (~60 lines)
14//!
15//! All unsafe blocks are:
16//! - **NULL-checked** before dereferencing
17//! - **Documented** with explicit safety requirements
18//! - **Isolated** only at FFI boundary, never in business logic
19//! - **Auditable**: ~0.9% of file (60 unsafe lines / 632 total)
20//!
21//! ## Case-Insensitive Matching: String Allocation Tradeoff
22//!
23//! Functions `directive_name_to_kind()` and `convert_clause()` allocate a String
24//! for case-insensitive matching (Fortran uses uppercase, C uses lowercase).
25//!
26//! **Why not optimize with `eq_ignore_ascii_case()`?**
27//! - Constants generator (`src/constants_gen.rs`) requires `match` expressions
28//! - Parser uses syn crate to extract directive/clause mappings from AST
29//! - Cannot parse if-else chains → must use `match normalized_name.as_str()`
30//! - String allocation is necessary for match arm patterns
31//!
32//! **Is this a performance issue?**
33//! - No: These functions are called once per directive/clause at API boundary
34//! - Typical usage: Parse a few dozen directives in an entire program
35//! - String allocation cost is negligible compared to parsing overhead
36//!
37//! **Future optimization path**: Update `constants_gen.rs` to parse if-else chains,
38//! then use `eq_ignore_ascii_case()` without allocations.
39//!
40//! ## Learning Rust: Why Unsafe is Needed at FFI Boundary
41//!
42//! 1. **C strings** → Rust strings: `CStr::from_ptr()` requires unsafe
43//! 2. **Memory ownership** transfer: `Box::into_raw()` / `Box::from_raw()`
44//! 3. **Raw pointer dereferencing**: C passes pointers, we must dereference
45//!
46//! Safe Rust cannot verify C's guarantees, so we explicitly document them.
47//!
48//! ## C Caller Responsibilities
49//!
50//! C callers MUST:
51//! - ✅ Check for NULL returns before use
52//! - ✅ Call `_free()` functions to prevent memory leaks
53//! - ✅ Never use pointers after calling `_free()`
54//! - ✅ Pass only valid null-terminated strings
55//! - ✅ Not modify strings returned by this API
56
57use std::ffi::{CStr, CString};
58use std::mem::ManuallyDrop;
59use std::os::raw::c_char;
60use std::ptr;
61
62use crate::lexer::Language;
63use crate::parser::{openmp, parse_omp_directive, Clause, ClauseKind};
64
65// ============================================================================
66// Language Constants for Fortran Support
67// ============================================================================
68
69/// C language (default) - uses #pragma omp
70pub const ROUP_LANG_C: i32 = 0;
71
72/// Fortran free-form - uses !$OMP sentinel
73pub const ROUP_LANG_FORTRAN_FREE: i32 = 1;
74
75/// Fortran fixed-form - uses !$OMP or C$OMP in columns 1-6
76pub const ROUP_LANG_FORTRAN_FIXED: i32 = 2;
77
78// ============================================================================
79// Constants Documentation
80// ============================================================================
81//
82// SINGLE SOURCE OF TRUTH: This file defines all directive and clause kind codes.
83//
84// The constants are defined in:
85// - directive_name_to_kind() function (directive codes 0-16)
86// - convert_clause() function (clause codes 0-11)
87//
88// For C/C++ usage:
89// - build.rs auto-generates src/roup_constants.h with #define macros
90// - The header provides compile-time constants for switch/case statements
91// - Never modify roup_constants.h directly - edit this file instead
92//
93// Maintenance: When adding new directives/clauses:
94// 1. Update directive_name_to_kind() or convert_clause() in this file
95// 2. Run `cargo build` to regenerate roup_constants.h
96// 3. The header will automatically include your new constants
97
98// ============================================================================
99// C-Compatible Types
100// ============================================================================
101//
102// Learning Rust: FFI Type Safety
103// ===============================
104// The `#[repr(C)]` attribute ensures these types have the same memory layout
105// as C structs. This is crucial for FFI safety:
106//
107// - Rust's default layout is undefined and may reorder fields
108// - C expects specific field ordering and sizes
109// - `#[repr(C)]` guarantees C-compatible layout
110//
111// Without `#[repr(C)]`, passing these to C would cause undefined behavior!
112
113/// Opaque directive type (C-compatible)
114///
115/// Represents a parsed OpenMP directive with its clauses.
116/// C sees this as an opaque pointer - internal structure is hidden.
117#[repr(C)]
118pub struct OmpDirective {
119    name: *const c_char,     // Directive name (e.g., "parallel")
120    clauses: Vec<OmpClause>, // Associated clauses
121}
122
123/// Opaque clause type (C-compatible)
124///
125/// Represents a single clause within a directive.
126/// Uses tagged union pattern for clause-specific data.
127#[repr(C)]
128pub struct OmpClause {
129    kind: i32,        // Clause type (num_threads=0, schedule=7, etc.)
130    data: ClauseData, // Clause-specific data (union)
131}
132
133/// Clause-specific data stored in a C union
134///
135/// Learning Rust: Why ManuallyDrop?
136/// =================================
137/// Unions in Rust don't know which variant is active, so they can't
138/// automatically call destructors. ManuallyDrop prevents automatic drops
139/// and lets us manually free resources when we know which variant is active.
140#[repr(C)]
141union ClauseData {
142    schedule: ManuallyDrop<ScheduleData>,
143    reduction: ManuallyDrop<ReductionData>,
144    default: i32,
145    variables: *mut OmpStringList,
146}
147
148/// Schedule clause data (static, dynamic, guided, etc.)
149#[repr(C)]
150#[derive(Copy, Clone)]
151struct ScheduleData {
152    kind: i32, // 0=static, 1=dynamic, 2=guided, 3=auto, 4=runtime
153}
154
155/// Reduction clause data (operator and variables)
156#[repr(C)]
157#[derive(Copy, Clone)]
158struct ReductionData {
159    operator: i32, // 0=+, 1=-, 2=*, 6=&&, 7=||, 8=min, 9=max
160}
161
162/// Iterator over clauses
163///
164/// Provides sequential access to directive's clauses.
165/// Holds raw pointers to avoid ownership issues at FFI boundary.
166#[repr(C)]
167pub struct OmpClauseIterator {
168    clauses: Vec<*const OmpClause>, // Pointers to clauses
169    index: usize,                   // Current position
170}
171
172/// List of strings (for variable names in clauses)
173///
174/// Used for private, shared, reduction variable lists.
175#[repr(C)]
176pub struct OmpStringList {
177    items: Vec<*const c_char>, // NULL-terminated C strings
178}
179
180// ============================================================================
181// Parse Function (UNSAFE BLOCK 1-2)
182// ============================================================================
183
184/// Parse an OpenMP directive from a C string.
185///
186/// ## Parameters
187/// - `input`: Null-terminated C string containing the directive
188///
189/// ## Returns
190/// - Pointer to `OmpDirective` on success
191/// - NULL on parse failure or NULL input
192///
193/// ## Safety
194/// Caller must:
195/// - Pass valid null-terminated C string or NULL
196/// - Call `roup_directive_free()` on the returned pointer
197///
198/// ## Example
199/// ```c
200/// OmpDirective* dir = roup_parse("#pragma omp parallel");
201/// if (dir) {
202///     // use directive
203///     roup_directive_free(dir);
204/// }
205/// ```
206#[no_mangle]
207pub extern "C" fn roup_parse(input: *const c_char) -> *mut OmpDirective {
208    // NULL check
209    if input.is_null() {
210        return ptr::null_mut();
211    }
212
213    // UNSAFE BLOCK 1: Convert C string to Rust &str
214    // Safety: Caller guarantees valid null-terminated C string
215    let c_str = unsafe { CStr::from_ptr(input) };
216
217    let rust_str = match c_str.to_str() {
218        Ok(s) => s,
219        Err(_) => return ptr::null_mut(), // Invalid UTF-8
220    };
221
222    // Parse using safe Rust parser
223    let directive = match parse_omp_directive(rust_str) {
224        Ok((_, dir)) => dir,
225        Err(_) => return ptr::null_mut(), // Parse error
226    };
227
228    // Convert to C-compatible format
229    let c_directive = OmpDirective {
230        name: allocate_c_string(directive.name.as_ref()),
231        clauses: directive
232            .clauses
233            .into_iter()
234            .map(|c| convert_clause(&c))
235            .collect(),
236    };
237
238    // UNSAFE BLOCK 2: Convert Box to raw pointer for C
239    // Safety: Caller will call roup_directive_free() to deallocate
240    Box::into_raw(Box::new(c_directive))
241}
242
243/// Free a directive allocated by `roup_parse()`.
244///
245/// ## Safety
246/// - Must only be called once per directive
247/// - Pointer must be from `roup_parse()`
248/// - Do not use pointer after calling this function
249#[no_mangle]
250pub extern "C" fn roup_directive_free(directive: *mut OmpDirective) {
251    if directive.is_null() {
252        return;
253    }
254
255    // UNSAFE BLOCK 3: Convert raw pointer back to Box for deallocation
256    // Safety: Pointer came from Box::into_raw in roup_parse
257    unsafe {
258        let boxed = Box::from_raw(directive);
259
260        // Free the name string (was allocated with CString::into_raw)
261        if !boxed.name.is_null() {
262            drop(CString::from_raw(boxed.name as *mut c_char));
263        }
264
265        // Free clause data
266        for clause in &boxed.clauses {
267            free_clause_data(clause);
268        }
269
270        // Box is dropped here, freeing memory
271    }
272}
273
274/// Parse an OpenMP directive with explicit language specification.
275///
276/// ## Parameters
277/// - `input`: Null-terminated string containing the directive
278/// - `language`: Language format (ROUP_LANG_C, ROUP_LANG_FORTRAN_FREE, ROUP_LANG_FORTRAN_FIXED)
279///
280/// ## Returns
281/// - Pointer to `OmpDirective` on success
282/// - `NULL` on error:
283///   - `input` is NULL
284///   - `language` is not a valid ROUP_LANG_* constant
285///   - `input` contains invalid UTF-8
286///   - Parse error (invalid OpenMP directive syntax)
287///
288/// ## Error Handling
289/// This function returns `NULL` for all error conditions without detailed error codes.
290/// There is no way to distinguish between different error types (invalid language,
291/// NULL input, UTF-8 error, or parse failure) from the return value alone.
292///
293/// Callers should:
294/// - Validate `language` parameter before calling (use only ROUP_LANG_* constants)
295/// - Ensure `input` is non-NULL and valid UTF-8
296/// - Verify directive syntax is correct
297/// - For debugging, enable logging or use a separate validation layer
298///
299/// For a version with detailed error codes, consider using the Rust API directly.
300///
301/// ## Example (Fortran free-form)
302/// ```c
303/// OmpDirective* dir = roup_parse_with_language("!$OMP PARALLEL PRIVATE(A)", ROUP_LANG_FORTRAN_FREE);
304/// if (dir) {
305///     // Use directive
306///     roup_directive_free(dir);
307/// } else {
308///     // Handle error: NULL, invalid language, invalid UTF-8, or parse failure
309///     fprintf(stderr, "Failed to parse directive\n");
310/// }
311/// ```
312#[no_mangle]
313pub extern "C" fn roup_parse_with_language(
314    input: *const c_char,
315    language: i32,
316) -> *mut OmpDirective {
317    // NULL check
318    if input.is_null() {
319        return ptr::null_mut();
320    }
321
322    // Convert language code to Language enum using explicit constants
323    // Return NULL for invalid language values
324    let lang = match language {
325        ROUP_LANG_C => Language::C,
326        ROUP_LANG_FORTRAN_FREE => Language::FortranFree,
327        ROUP_LANG_FORTRAN_FIXED => Language::FortranFixed,
328        _ => return ptr::null_mut(), // Invalid language value
329    };
330
331    // UNSAFE BLOCK: Convert C string to Rust &str
332    let c_str = unsafe { CStr::from_ptr(input) };
333
334    let rust_str = match c_str.to_str() {
335        Ok(s) => s,
336        Err(_) => return ptr::null_mut(),
337    };
338
339    // Create parser with specified language
340    let parser = openmp::parser().with_language(lang);
341
342    // Parse using language-aware parser
343    let directive = match parser.parse(rust_str) {
344        Ok((_, dir)) => dir,
345        Err(_) => return ptr::null_mut(),
346    };
347
348    // Convert to C-compatible format
349    let c_directive = OmpDirective {
350        name: allocate_c_string(directive.name.as_ref()),
351        clauses: directive
352            .clauses
353            .into_iter()
354            .map(|c| convert_clause(&c))
355            .collect(),
356    };
357
358    Box::into_raw(Box::new(c_directive))
359}
360
361/// Free a clause.
362#[no_mangle]
363pub extern "C" fn roup_clause_free(clause: *mut OmpClause) {
364    if clause.is_null() {
365        return;
366    }
367
368    unsafe {
369        let boxed = Box::from_raw(clause);
370        free_clause_data(&boxed);
371    }
372}
373
374// ============================================================================
375// Directive Query Functions (All Safe)
376// ============================================================================
377
378/// Get directive kind.
379///
380/// Returns -1 if directive is NULL.
381#[no_mangle]
382pub extern "C" fn roup_directive_kind(directive: *const OmpDirective) -> i32 {
383    if directive.is_null() {
384        return -1;
385    }
386
387    // UNSAFE BLOCK 4: Dereference pointer
388    // Safety: Caller guarantees valid pointer from roup_parse
389    unsafe {
390        let dir = &*directive;
391        directive_name_to_kind(dir.name)
392    }
393}
394
395/// Get number of clauses in a directive.
396///
397/// Returns 0 if directive is NULL.
398#[no_mangle]
399pub extern "C" fn roup_directive_clause_count(directive: *const OmpDirective) -> i32 {
400    if directive.is_null() {
401        return 0;
402    }
403
404    unsafe {
405        let dir = &*directive;
406        dir.clauses.len() as i32
407    }
408}
409
410/// Create an iterator over directive clauses.
411///
412/// Returns NULL if directive is NULL.
413/// Caller must call `roup_clause_iterator_free()`.
414#[no_mangle]
415pub extern "C" fn roup_directive_clauses_iter(
416    directive: *const OmpDirective,
417) -> *mut OmpClauseIterator {
418    if directive.is_null() {
419        return ptr::null_mut();
420    }
421
422    unsafe {
423        let dir = &*directive;
424        let iter = OmpClauseIterator {
425            clauses: dir.clauses.iter().map(|c| c as *const OmpClause).collect(),
426            index: 0,
427        };
428        Box::into_raw(Box::new(iter))
429    }
430}
431
432// ============================================================================
433// Iterator Functions (UNSAFE BLOCKS 5-6)
434// ============================================================================
435
436/// Get next clause from iterator.
437///
438/// ## Parameters
439/// - `iter`: Iterator from `roup_directive_clauses_iter()`
440/// - `out`: Output pointer to write clause pointer
441///
442/// ## Returns
443/// - 1 if clause available (written to `out`)
444/// - 0 if no more clauses or NULL inputs
445#[no_mangle]
446pub extern "C" fn roup_clause_iterator_next(
447    iter: *mut OmpClauseIterator,
448    out: *mut *const OmpClause,
449) -> i32 {
450    // NULL checks
451    if iter.is_null() || out.is_null() {
452        return 0;
453    }
454
455    // UNSAFE BLOCK 5: Dereference iterator
456    // Safety: Caller guarantees valid iterator pointer
457    unsafe {
458        let iterator = &mut *iter;
459
460        if iterator.index >= iterator.clauses.len() {
461            return 0; // No more items
462        }
463
464        let clause_ptr = iterator.clauses[iterator.index];
465        iterator.index += 1;
466
467        // UNSAFE BLOCK 6: Write to output pointer
468        // Safety: Caller guarantees valid output pointer
469        *out = clause_ptr;
470        1
471    }
472}
473
474/// Free clause iterator.
475#[no_mangle]
476pub extern "C" fn roup_clause_iterator_free(iter: *mut OmpClauseIterator) {
477    if iter.is_null() {
478        return;
479    }
480
481    unsafe {
482        drop(Box::from_raw(iter));
483    }
484}
485
486// ============================================================================
487// Clause Query Functions (UNSAFE BLOCKS 7-8)
488// ============================================================================
489
490/// Get clause kind.
491///
492/// Returns -1 if clause is NULL.
493#[no_mangle]
494pub extern "C" fn roup_clause_kind(clause: *const OmpClause) -> i32 {
495    if clause.is_null() {
496        return -1;
497    }
498
499    // UNSAFE BLOCK 7: Dereference clause
500    // Safety: Caller guarantees valid clause pointer
501    unsafe {
502        let c = &*clause;
503        c.kind
504    }
505}
506
507/// Get schedule kind from schedule clause.
508///
509/// Returns -1 if clause is NULL or not a schedule clause.
510#[no_mangle]
511pub extern "C" fn roup_clause_schedule_kind(clause: *const OmpClause) -> i32 {
512    if clause.is_null() {
513        return -1;
514    }
515
516    unsafe {
517        let c = &*clause;
518        if c.kind != 7 {
519            // Not a schedule clause
520            return -1;
521        }
522        c.data.schedule.kind
523    }
524}
525
526/// Get reduction operator from reduction clause.
527///
528/// Returns -1 if clause is NULL or not a reduction clause.
529#[no_mangle]
530pub extern "C" fn roup_clause_reduction_operator(clause: *const OmpClause) -> i32 {
531    if clause.is_null() {
532        return -1;
533    }
534
535    unsafe {
536        let c = &*clause;
537        if c.kind != 6 {
538            // Not a reduction clause
539            return -1;
540        }
541        c.data.reduction.operator
542    }
543}
544
545/// Get default data sharing from default clause.
546///
547/// Returns -1 if clause is NULL or not a default clause.
548#[no_mangle]
549pub extern "C" fn roup_clause_default_data_sharing(clause: *const OmpClause) -> i32 {
550    if clause.is_null() {
551        return -1;
552    }
553
554    unsafe {
555        let c = &*clause;
556        if c.kind != 11 {
557            // Not a default clause
558            return -1;
559        }
560        c.data.default
561    }
562}
563
564// ============================================================================
565// Variable List Functions (UNSAFE BLOCKS 9-11)
566// ============================================================================
567
568/// Get variable list from clause (private, shared, reduction, etc.).
569///
570/// Returns NULL if clause is NULL or has no variables.
571/// Caller must call `roup_string_list_free()`.
572#[no_mangle]
573pub extern "C" fn roup_clause_variables(clause: *const OmpClause) -> *mut OmpStringList {
574    if clause.is_null() {
575        return ptr::null_mut();
576    }
577
578    // UNSAFE BLOCK 8: Dereference clause for variable check
579    // Safety: Caller guarantees valid clause pointer
580    unsafe {
581        let c = &*clause;
582
583        // Check if this clause type has variables
584        // Kinds 2-5 are private/shared/firstprivate/lastprivate
585        if c.kind < 2 || c.kind > 6 {
586            return ptr::null_mut();
587        }
588
589        // For now, return empty list (would need clause parsing enhancement)
590        let list = OmpStringList { items: Vec::new() };
591        Box::into_raw(Box::new(list))
592    }
593}
594
595/// Get length of string list.
596///
597/// Returns 0 if list is NULL.
598#[no_mangle]
599pub extern "C" fn roup_string_list_len(list: *const OmpStringList) -> i32 {
600    if list.is_null() {
601        return 0;
602    }
603
604    unsafe {
605        let l = &*list;
606        l.items.len() as i32
607    }
608}
609
610/// Get string at index from list.
611///
612/// Returns NULL if list is NULL or index out of bounds.
613/// Returned pointer is valid until list is freed.
614#[no_mangle]
615pub extern "C" fn roup_string_list_get(list: *const OmpStringList, index: i32) -> *const c_char {
616    if list.is_null() || index < 0 {
617        return ptr::null();
618    }
619
620    // UNSAFE BLOCK 9: Dereference list
621    // Safety: Caller guarantees valid list pointer
622    unsafe {
623        let l = &*list;
624        let idx = index as usize;
625
626        if idx >= l.items.len() {
627            return ptr::null();
628        }
629
630        l.items[idx]
631    }
632}
633
634/// Free string list.
635#[no_mangle]
636pub extern "C" fn roup_string_list_free(list: *mut OmpStringList) {
637    if list.is_null() {
638        return;
639    }
640
641    // UNSAFE BLOCK 10: Free list and strings
642    // Safety: Pointer from roup_clause_variables
643    unsafe {
644        let boxed = Box::from_raw(list);
645
646        // Free each C string (was allocated with CString::into_raw)
647        for item_ptr in &boxed.items {
648            if !item_ptr.is_null() {
649                drop(CString::from_raw(*item_ptr as *mut c_char));
650            }
651        }
652
653        // Box dropped here
654    }
655}
656
657// ============================================================================
658// Helper Functions (Internal - Not Exported to C)
659// ============================================================================
660//
661// These functions handle conversion between Rust and C representations.
662// They're not exported because C doesn't need to call them directly.
663
664/// Allocate a C string from Rust &str.
665///
666/// Creates a heap-allocated, null-terminated C string from Rust string.
667/// The returned pointer must be freed with CString::from_raw() later.
668///
669/// ## How it works:
670/// 1. CString::new() creates null-terminated copy
671/// 2. into_raw() gives us ownership of the pointer
672/// 3. Caller (or roup_directive_free) must eventually free it
673fn allocate_c_string(s: &str) -> *const c_char {
674    let c_string = std::ffi::CString::new(s).unwrap();
675    c_string.into_raw() as *const c_char
676}
677
678/// Convert Rust Clause to C-compatible OmpClause.
679///
680/// Maps clause names to integer kind codes (C doesn't have Rust enums).
681/// Each clause type gets a unique ID and appropriate data representation.
682///
683/// ## Clause Kind Mapping:
684/// - 0 = num_threads    - 6 = reduction
685/// - 1 = if             - 7 = schedule
686/// - 2 = private        - 8 = collapse
687/// - 3 = shared         - 9 = ordered
688/// - 4 = firstprivate   - 10 = nowait
689/// - 5 = lastprivate    - 11 = default
690/// - 999 = unknown
691fn convert_clause(clause: &Clause) -> OmpClause {
692    // Normalize clause name to lowercase for case-insensitive matching
693    // (Fortran clauses are uppercase, C clauses are lowercase)
694    // Note: One String allocation per clause is acceptable at C API boundary.
695    // Alternative (build-time constant map) requires updating constants_gen.rs
696    // to parse if-else chains instead of match expressions.
697    let normalized_name = clause.name.to_ascii_lowercase();
698
699    let (kind, data) = match normalized_name.as_str() {
700        "num_threads" => (0, ClauseData { default: 0 }),
701        "if" => (1, ClauseData { default: 0 }),
702        "private" => (
703            2,
704            ClauseData {
705                variables: ptr::null_mut(),
706            },
707        ),
708        "shared" => (
709            3,
710            ClauseData {
711                variables: ptr::null_mut(),
712            },
713        ),
714        "firstprivate" => (
715            4,
716            ClauseData {
717                variables: ptr::null_mut(),
718            },
719        ),
720        "lastprivate" => (
721            5,
722            ClauseData {
723                variables: ptr::null_mut(),
724            },
725        ),
726        "reduction" => {
727            let operator = parse_reduction_operator(clause);
728            (
729                6,
730                ClauseData {
731                    reduction: ManuallyDrop::new(ReductionData { operator }),
732                },
733            )
734        }
735        "schedule" => {
736            let schedule_kind = parse_schedule_kind(clause);
737            (
738                7,
739                ClauseData {
740                    schedule: ManuallyDrop::new(ScheduleData {
741                        kind: schedule_kind,
742                    }),
743                },
744            )
745        }
746        "collapse" => (8, ClauseData { default: 0 }),
747        "ordered" => (9, ClauseData { default: 0 }),
748        "nowait" => (10, ClauseData { default: 0 }),
749        "default" => {
750            let default_kind = parse_default_kind(clause);
751            (
752                11,
753                ClauseData {
754                    default: default_kind,
755                },
756            )
757        }
758        _ => (999, ClauseData { default: 0 }), // Unknown
759    };
760
761    OmpClause { kind, data }
762}
763
764/// Parse reduction operator from clause arguments.
765///
766/// Extracts the operator from reduction clause like "reduction(+: sum)".
767/// Returns integer code for the operator type.
768///
769/// ## Operator Codes:
770/// - 0 = +  (addition)      - 5 = ^  (bitwise XOR)
771/// - 1 = -  (subtraction)   - 6 = && (logical AND)
772/// - 2 = *  (multiplication) - 7 = || (logical OR)
773/// - 3 = &  (bitwise AND)   - 8 = min
774/// - 4 = |  (bitwise OR)    - 9 = max
775fn parse_reduction_operator(clause: &Clause) -> i32 {
776    // Look for operator in clause kind
777    if let ClauseKind::Parenthesized(ref args) = clause.kind {
778        let args = args.as_ref();
779        // Operators (+, -, *, etc.) are ASCII symbols - no case conversion needed
780        if args.contains('+') && !args.contains("++") {
781            return 0; // Plus
782        } else if args.contains('-') && !args.contains("--") {
783            return 1; // Minus
784        } else if args.contains('*') {
785            return 2; // Times
786        } else if args.contains('&') && !args.contains("&&") {
787            return 3; // BitwiseAnd
788        } else if args.contains('|') && !args.contains("||") {
789            return 4; // BitwiseOr
790        } else if args.contains('^') {
791            return 5; // BitwiseXor
792        } else if args.contains("&&") {
793            return 6; // LogicalAnd
794        } else if args.contains("||") {
795            return 7; // LogicalOr
796        }
797
798        // For text keywords (min, max), normalize once for case-insensitive comparison
799        let args_lower = args.to_ascii_lowercase();
800        if args_lower.contains("min") {
801            return 8; // Min
802        } else if args_lower.contains("max") {
803            return 9; // Max
804        }
805    }
806    0 // Default to plus
807}
808
809/// Parse schedule kind from clause arguments.
810///
811/// Extracts schedule type from clause like "schedule(dynamic, 4)".
812/// Returns integer code for the schedule policy.
813///
814/// ## Schedule Codes:
815/// - 0 = static   (default, divide iterations evenly)
816/// - 1 = dynamic  (distribute at runtime)
817/// - 2 = guided   (decreasing chunk sizes)
818/// - 3 = auto     (compiler decides)
819/// - 4 = runtime  (OMP_SCHEDULE environment variable)
820fn parse_schedule_kind(clause: &Clause) -> i32 {
821    if let ClauseKind::Parenthesized(ref args) = clause.kind {
822        let args = args.as_ref();
823        // Case-insensitive keyword matching without String allocation
824        // Check common case variants (lowercase, uppercase, title case)
825        if args.contains("static") || args.contains("STATIC") || args.contains("Static") {
826            return 0;
827        } else if args.contains("dynamic") || args.contains("DYNAMIC") || args.contains("Dynamic") {
828            return 1;
829        } else if args.contains("guided") || args.contains("GUIDED") || args.contains("Guided") {
830            return 2;
831        } else if args.contains("auto") || args.contains("AUTO") || args.contains("Auto") {
832            return 3;
833        } else if args.contains("runtime") || args.contains("RUNTIME") || args.contains("Runtime") {
834            return 4;
835        }
836    }
837    0 // Default to static
838}
839
840/// Parse default clause data-sharing attribute.
841///
842/// Extracts the default sharing from clause like "default(shared)".
843/// Returns integer code for the default policy.
844///
845/// ## Default Codes:
846/// - 0 = shared (all variables shared by default)
847/// - 1 = none   (must explicitly declare all variables)
848fn parse_default_kind(clause: &Clause) -> i32 {
849    if let ClauseKind::Parenthesized(ref args) = clause.kind {
850        let args = args.as_ref();
851        // Case-insensitive keyword matching without String allocation
852        // Check common case variants (lowercase, uppercase, title case)
853        if args.contains("shared") || args.contains("SHARED") || args.contains("Shared") {
854            return 0;
855        } else if args.contains("none") || args.contains("NONE") || args.contains("None") {
856            return 1;
857        }
858    }
859    0 // Default to shared
860}
861
862/// Convert directive name to kind enum code.
863///
864/// Maps directive names (parallel, for, task, etc.) to integer codes
865/// so C code can use switch statements instead of string comparisons.
866///
867/// ## Directive Codes:
868/// - 0 = parallel     - 5 = critical
869/// - 1 = for          - 6 = atomic
870/// - 2 = sections     - 7 = barrier
871/// - 3 = single       - 8 = master
872/// - 4 = task         - 9 = teams
873/// - 10 = target      - 11 = distribute
874/// - -1 = NULL/unknown
875fn directive_name_to_kind(name: *const c_char) -> i32 {
876    if name.is_null() {
877        return -1;
878    }
879
880    // UNSAFE BLOCK 11: Read directive name
881    // Safety: name pointer is valid (from our own allocation)
882    unsafe {
883        let c_str = CStr::from_ptr(name);
884        let name_str = c_str.to_str().unwrap_or("");
885
886        // Case-insensitive directive name matching via to_lowercase()
887        // Note: This allocates a String. While eq_ignore_ascii_case() would be more efficient,
888        // the build system's constant parser requires a match expression with string literals.
889        // The performance impact is negligible for the C API boundary.
890        //
891        // Fortran DO variants map to same codes as their C FOR equivalents:
892        // - "do" -> 1 (same as "for")
893        // - "parallel do" -> 0 (same as "parallel for")
894        // - "distribute parallel do" -> 15 (same as "distribute parallel for")
895        // - "target parallel do" -> 13 (same as "target parallel for")
896        // etc.
897        match name_str.to_lowercase().as_str() {
898            // Parallel directives (kind 0)
899            "parallel" => 0,
900            "parallel for" => 0,
901            "parallel do" => 0, // Fortran variant
902            "parallel for simd" => 0,
903            "parallel do simd" => 0, // Fortran variant
904            "parallel sections" => 0,
905
906            // For/Do directives (kind 1)
907            "for" => 1,
908            "do" => 1, // Fortran variant
909            "for simd" => 1,
910            "do simd" => 1, // Fortran variant
911
912            // Other basic directives
913            "sections" => 2,
914            "single" => 3,
915            "task" => 4,
916            "master" => 5,
917            "critical" => 6,
918            "barrier" => 7,
919            "taskwait" => 8,
920            "taskgroup" => 9,
921            "atomic" => 10,
922            "flush" => 11,
923            "ordered" => 12,
924
925            // Target directives (kind 13)
926            "target" => 13,
927            "target teams" => 13,
928            "target parallel" => 13,
929            "target parallel for" => 13,
930            "target parallel do" => 13, // Fortran variant
931            "target parallel for simd" => 13,
932            "target parallel do simd" => 13, // Fortran variant
933            "target teams distribute" => 13,
934            "target teams distribute parallel for" => 13,
935            "target teams distribute parallel do" => 13, // Fortran variant
936            "target teams distribute parallel for simd" => 13,
937            "target teams distribute parallel do simd" => 13, // Fortran variant
938
939            // Teams directives (kind 14)
940            "teams" => 14,
941            "teams distribute" => 14,
942            "teams distribute parallel for" => 14,
943            "teams distribute parallel do" => 14, // Fortran variant
944            "teams distribute parallel for simd" => 14,
945            "teams distribute parallel do simd" => 14, // Fortran variant
946
947            // Distribute directives (kind 15)
948            "distribute" => 15,
949            "distribute parallel for" => 15,
950            "distribute parallel do" => 15, // Fortran variant
951            "distribute parallel for simd" => 15,
952            "distribute parallel do simd" => 15, // Fortran variant
953            "distribute simd" => 15,
954
955            // Metadirective (kind 16)
956            "metadirective" => 16,
957
958            // Unknown directive
959            _ => 999,
960        }
961    }
962}
963
964/// Free clause-specific data.
965/// Free clause-specific data (internal helper).
966///
967/// Handles cleanup for union types where different variants need different
968/// cleanup strategies. Currently only variable lists need explicit freeing.
969///
970/// ## Design Note:
971/// Most clause data (schedule, reduction, default) are Copy types that don't
972/// need cleanup. Only heap-allocated variable lists need explicit freeing.
973///
974/// ## IMPORTANT: ClauseData is a C union!
975/// The ClauseData union has 4 fields, but only ONE is active per clause:
976///   - `variables: *mut OmpStringList` - used by kinds 2-5 (private/shared/firstprivate/lastprivate)
977///   - `reduction: ReductionData` - used by kind 6 (reduction operator only, NO variables pointer)
978///   - `schedule: ScheduleData` - used by kind 7 (schedule policy only)
979///   - `default: i32` - used by other kinds
980///
981/// Reduction clauses (kind 6) do NOT use the `variables` field. Trying to free
982/// clause.data.variables on a reduction clause would read garbage memory from the
983/// wrong union variant (the bytes of ReductionData::operator interpreted as a pointer).
984fn free_clause_data(clause: &OmpClause) {
985    unsafe {
986        // Free variable lists if present
987        // Clause kinds with variable lists (see convert_clause):
988        //   2 = private, 3 = shared, 4 = firstprivate, 5 = lastprivate
989        // Other kinds use different union fields:
990        //   6 = reduction (uses .reduction field, NOT .variables)
991        //   7 = schedule (uses .schedule field, NOT .variables)
992        if clause.kind >= 2 && clause.kind <= 5 {
993            let vars_ptr = clause.data.variables;
994            if !vars_ptr.is_null() {
995                roup_string_list_free(vars_ptr);
996            }
997        }
998    }
999}
1000
1001// ============================================================================
1002// END OF C API IMPLEMENTATION
1003// ============================================================================
1004//
1005// Summary: This C API provides a minimal unsafe FFI layer over safe Rust code.
1006//
1007// Total Safety Profile:
1008// - 18 unsafe blocks (~60 lines = 0.9% of file)
1009// - All unsafe confined to FFI boundary
1010// - Core parsing logic remains 100% safe Rust
1011//
1012// Design Principles Applied:
1013// 1. ✅ Minimal unsafe: Only at C boundary, not in business logic
1014// 2. ✅ Direct pointers: Simple, predictable, C-friendly
1015// 3. ✅ Caller responsibility: C manages memory lifetime explicitly
1016// 4. ✅ Fail-fast: NULL returns on any error
1017// 5. ✅ No hidden state: Stateless API, no global variables
1018//
1019// Why This Approach Works:
1020// - C programmers understand manual memory management
1021// - Performance: No overhead from handle tables or reference counting
1022// - Simplicity: Direct mapping between Rust types and C expectations
1023// - Safety: Core parser is safe Rust, only FFI layer has unsafe
1024//
1025// Future Considerations for v1.0.0:
1026// - Thread safety annotations (Rust types are Send/Sync, C must ensure too)
1027// - Comprehensive error reporting (currently just NULL on error)
1028// - Semantic validation beyond parsing (requires deeper OpenMP knowledge)
1029
1030#[cfg(test)]
1031mod tests {
1032    use super::*;
1033
1034    #[test]
1035    fn test_fortran_directive_name_normalization() {
1036        // Test that uppercase Fortran directive names are properly normalized
1037        // This ensures C API can handle both C (lowercase) and Fortran (uppercase) directives
1038
1039        // Test basic Fortran PARALLEL directive
1040        let fortran_input = CString::new("!$OMP PARALLEL").unwrap();
1041        let directive = roup_parse_with_language(fortran_input.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1042        assert!(
1043            !directive.is_null(),
1044            "Failed to parse Fortran PARALLEL directive"
1045        );
1046
1047        let kind = roup_directive_kind(directive);
1048        assert_eq!(
1049            kind, 0,
1050            "PARALLEL directive should have kind 0, got {}",
1051            kind
1052        );
1053
1054        roup_directive_free(directive);
1055
1056        // Test Fortran DO directive (equivalent to C FOR)
1057        let fortran_do = CString::new("!$OMP DO").unwrap();
1058        let directive = roup_parse_with_language(fortran_do.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1059        assert!(!directive.is_null(), "Failed to parse Fortran DO directive");
1060
1061        let kind = roup_directive_kind(directive);
1062        assert_eq!(
1063            kind, 1,
1064            "DO directive should have kind 1 (same as FOR), got {}",
1065            kind
1066        );
1067
1068        roup_directive_free(directive);
1069
1070        // Test compound Fortran PARALLEL DO directive
1071        let fortran_parallel_do = CString::new("!$OMP PARALLEL DO").unwrap();
1072        let directive =
1073            roup_parse_with_language(fortran_parallel_do.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1074        assert!(
1075            !directive.is_null(),
1076            "Failed to parse Fortran PARALLEL DO directive"
1077        );
1078
1079        let kind = roup_directive_kind(directive);
1080        assert_eq!(
1081            kind, 0,
1082            "PARALLEL DO directive should have kind 0 (composite), got {}",
1083            kind
1084        );
1085
1086        roup_directive_free(directive);
1087    }
1088
1089    #[test]
1090    fn test_fortran_clause_name_normalization() {
1091        // Test that uppercase Fortran clause names are properly normalized
1092        // This ensures convert_clause() can handle both C and Fortran syntax
1093
1094        // Test Fortran PRIVATE clause
1095        let fortran_input = CString::new("!$OMP PARALLEL PRIVATE(A,B)").unwrap();
1096        let directive = roup_parse_with_language(fortran_input.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1097        assert!(
1098            !directive.is_null(),
1099            "Failed to parse Fortran directive with PRIVATE clause"
1100        );
1101
1102        let clause_count = roup_directive_clause_count(directive);
1103        assert_eq!(
1104            clause_count, 1,
1105            "Should have 1 clause, got {}",
1106            clause_count
1107        );
1108
1109        // Use iterator to get first clause
1110        let iter = roup_directive_clauses_iter(directive);
1111        assert!(!iter.is_null(), "Failed to create clause iterator");
1112
1113        let mut clause: *const OmpClause = ptr::null();
1114        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1115        assert_eq!(has_clause, 1, "Should have a clause");
1116        assert!(!clause.is_null(), "Clause pointer should not be null");
1117
1118        let clause_kind = roup_clause_kind(clause);
1119        assert_eq!(
1120            clause_kind, 2,
1121            "PRIVATE clause should have kind 2, got {}",
1122            clause_kind
1123        );
1124
1125        roup_clause_iterator_free(iter);
1126        roup_directive_free(directive);
1127
1128        // Test Fortran REDUCTION clause
1129        let fortran_reduction = CString::new("!$OMP DO REDUCTION(+:SUM)").unwrap();
1130        let directive =
1131            roup_parse_with_language(fortran_reduction.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1132        assert!(
1133            !directive.is_null(),
1134            "Failed to parse Fortran DO with REDUCTION clause"
1135        );
1136
1137        let clause_count = roup_directive_clause_count(directive);
1138        assert_eq!(
1139            clause_count, 1,
1140            "Should have 1 clause, got {}",
1141            clause_count
1142        );
1143
1144        let iter = roup_directive_clauses_iter(directive);
1145        let mut clause: *const OmpClause = ptr::null();
1146        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1147        assert_eq!(has_clause, 1, "Should have a clause");
1148
1149        let clause_kind = roup_clause_kind(clause);
1150        assert_eq!(
1151            clause_kind, 6,
1152            "REDUCTION clause should have kind 6, got {}",
1153            clause_kind
1154        );
1155
1156        roup_clause_iterator_free(iter);
1157        roup_directive_free(directive);
1158
1159        // Test Fortran SCHEDULE clause
1160        let fortran_schedule = CString::new("!$OMP DO SCHEDULE(DYNAMIC)").unwrap();
1161        let directive = roup_parse_with_language(fortran_schedule.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1162        assert!(
1163            !directive.is_null(),
1164            "Failed to parse Fortran DO with SCHEDULE clause"
1165        );
1166
1167        let clause_count = roup_directive_clause_count(directive);
1168        assert_eq!(
1169            clause_count, 1,
1170            "Should have 1 clause, got {}",
1171            clause_count
1172        );
1173
1174        let iter = roup_directive_clauses_iter(directive);
1175        let mut clause: *const OmpClause = ptr::null();
1176        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1177        assert_eq!(has_clause, 1, "Should have a clause");
1178
1179        let clause_kind = roup_clause_kind(clause);
1180        assert_eq!(
1181            clause_kind, 7,
1182            "SCHEDULE clause should have kind 7, got {}",
1183            clause_kind
1184        );
1185
1186        roup_clause_iterator_free(iter);
1187        roup_directive_free(directive);
1188    }
1189
1190    #[test]
1191    fn test_case_insensitive_matching() {
1192        // Verify that both lowercase and uppercase inputs work correctly
1193
1194        // C-style lowercase
1195        let c_input = CString::new("#pragma omp parallel for").unwrap();
1196        let c_directive = roup_parse(c_input.as_ptr());
1197        assert!(!c_directive.is_null());
1198        let c_kind = roup_directive_kind(c_directive);
1199
1200        // Fortran-style uppercase
1201        let fortran_input = CString::new("!$OMP PARALLEL DO").unwrap();
1202        let fortran_directive =
1203            roup_parse_with_language(fortran_input.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1204        assert!(!fortran_directive.is_null());
1205        let fortran_kind = roup_directive_kind(fortran_directive);
1206
1207        // Both should map to same kind (0 for parallel/composite)
1208        assert_eq!(
1209            c_kind, fortran_kind,
1210            "C 'parallel for' and Fortran 'PARALLEL DO' should have same kind"
1211        );
1212
1213        roup_directive_free(c_directive);
1214        roup_directive_free(fortran_directive);
1215    }
1216
1217    #[test]
1218    fn test_fortran_schedule_clause_case_insensitive() {
1219        // Test that SCHEDULE clause arguments are case-insensitive
1220        // Fortran: !$OMP DO SCHEDULE(DYNAMIC) should work same as C: schedule(dynamic)
1221
1222        // Test uppercase DYNAMIC
1223        let fortran_dynamic = CString::new("!$OMP DO SCHEDULE(DYNAMIC)").unwrap();
1224        let directive = roup_parse_with_language(fortran_dynamic.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1225        assert!(!directive.is_null(), "Failed to parse SCHEDULE(DYNAMIC)");
1226
1227        let iter = roup_directive_clauses_iter(directive);
1228        let mut clause: *const OmpClause = ptr::null();
1229        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1230        assert_eq!(has_clause, 1, "Should have schedule clause");
1231
1232        let schedule_kind = roup_clause_schedule_kind(clause);
1233        assert_eq!(
1234            schedule_kind, 1,
1235            "SCHEDULE(DYNAMIC) should have kind 1 (dynamic), got {}",
1236            schedule_kind
1237        );
1238
1239        roup_clause_iterator_free(iter);
1240        roup_directive_free(directive);
1241
1242        // Test uppercase GUIDED
1243        let fortran_guided = CString::new("!$OMP DO SCHEDULE(GUIDED, 10)").unwrap();
1244        let directive = roup_parse_with_language(fortran_guided.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1245        assert!(!directive.is_null(), "Failed to parse SCHEDULE(GUIDED)");
1246
1247        let iter = roup_directive_clauses_iter(directive);
1248        let mut clause: *const OmpClause = ptr::null();
1249        roup_clause_iterator_next(iter, &mut clause);
1250
1251        let schedule_kind = roup_clause_schedule_kind(clause);
1252        assert_eq!(
1253            schedule_kind, 2,
1254            "SCHEDULE(GUIDED) should have kind 2, got {}",
1255            schedule_kind
1256        );
1257
1258        roup_clause_iterator_free(iter);
1259        roup_directive_free(directive);
1260    }
1261
1262    #[test]
1263    fn test_fortran_default_clause_case_insensitive() {
1264        // Test that DEFAULT clause arguments are case-insensitive
1265        // Fortran: !$OMP PARALLEL DEFAULT(NONE) should work same as C: default(none)
1266
1267        // Test uppercase NONE
1268        let fortran_none = CString::new("!$OMP PARALLEL DEFAULT(NONE)").unwrap();
1269        let directive = roup_parse_with_language(fortran_none.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1270        assert!(!directive.is_null(), "Failed to parse DEFAULT(NONE)");
1271
1272        let iter = roup_directive_clauses_iter(directive);
1273        let mut clause: *const OmpClause = ptr::null();
1274        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1275        assert_eq!(has_clause, 1, "Should have default clause");
1276
1277        let default_kind = roup_clause_default_data_sharing(clause);
1278        assert_eq!(
1279            default_kind, 1,
1280            "DEFAULT(NONE) should have kind 1 (none), got {}",
1281            default_kind
1282        );
1283
1284        roup_clause_iterator_free(iter);
1285        roup_directive_free(directive);
1286
1287        // Test uppercase SHARED (verify it still works)
1288        let fortran_shared = CString::new("!$OMP PARALLEL DEFAULT(SHARED)").unwrap();
1289        let directive = roup_parse_with_language(fortran_shared.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1290        assert!(!directive.is_null(), "Failed to parse DEFAULT(SHARED)");
1291
1292        let iter = roup_directive_clauses_iter(directive);
1293        let mut clause: *const OmpClause = ptr::null();
1294        roup_clause_iterator_next(iter, &mut clause);
1295
1296        let default_kind = roup_clause_default_data_sharing(clause);
1297        assert_eq!(
1298            default_kind, 0,
1299            "DEFAULT(SHARED) should have kind 0, got {}",
1300            default_kind
1301        );
1302
1303        roup_clause_iterator_free(iter);
1304        roup_directive_free(directive);
1305    }
1306
1307    #[test]
1308    fn test_fortran_reduction_clause_case_insensitive() {
1309        // Test that REDUCTION clause operators work with uppercase (e.g., MIN, MAX)
1310
1311        // Test uppercase MIN
1312        let fortran_min = CString::new("!$OMP PARALLEL REDUCTION(MIN:X)").unwrap();
1313        let directive = roup_parse_with_language(fortran_min.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1314        assert!(!directive.is_null(), "Failed to parse REDUCTION(MIN:X)");
1315
1316        let iter = roup_directive_clauses_iter(directive);
1317        let mut clause: *const OmpClause = ptr::null();
1318        let has_clause = roup_clause_iterator_next(iter, &mut clause);
1319        assert_eq!(has_clause, 1, "Should have reduction clause");
1320
1321        let reduction_op = roup_clause_reduction_operator(clause);
1322        assert_eq!(
1323            reduction_op, 8,
1324            "REDUCTION(MIN:X) should have operator 8 (min), got {}",
1325            reduction_op
1326        );
1327
1328        roup_clause_iterator_free(iter);
1329        roup_directive_free(directive);
1330
1331        // Test uppercase MAX
1332        let fortran_max = CString::new("!$OMP DO REDUCTION(MAX:RESULT)").unwrap();
1333        let directive = roup_parse_with_language(fortran_max.as_ptr(), ROUP_LANG_FORTRAN_FREE);
1334        assert!(
1335            !directive.is_null(),
1336            "Failed to parse REDUCTION(MAX:RESULT)"
1337        );
1338
1339        let iter = roup_directive_clauses_iter(directive);
1340        let mut clause: *const OmpClause = ptr::null();
1341        roup_clause_iterator_next(iter, &mut clause);
1342
1343        let reduction_op = roup_clause_reduction_operator(clause);
1344        assert_eq!(
1345            reduction_op, 9,
1346            "REDUCTION(MAX:RESULT) should have operator 9 (max), got {}",
1347            reduction_op
1348        );
1349
1350        roup_clause_iterator_free(iter);
1351        roup_directive_free(directive);
1352    }
1353}