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}