Skip to content

Commit 22a2c56

Browse files
Copilotgerlero
andcommitted
Add Rust module with _skip function implementation
Co-authored-by: gerlero <[email protected]>
1 parent e11339d commit 22a2c56

File tree

5 files changed

+353
-2
lines changed

5 files changed

+353
-2
lines changed

Cargo.lock

Lines changed: 180 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "foamlib"
3+
version = "1.5.4"
4+
edition = "2021"
5+
6+
[lib]
7+
name = "foamlib_rust"
8+
crate-type = ["cdylib"]
9+
path = "rust_src/lib.rs"
10+
11+
[dependencies]
12+
pyo3 = { version = "0.23", features = ["extension-module"] }

pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
2-
requires = ["uv_build>=0.9,<0.10"]
3-
build-backend = "uv_build"
2+
requires = ["maturin>=1.0,<2.0"]
3+
build-backend = "maturin"
44

55
[project]
66
name = "foamlib"
@@ -119,3 +119,7 @@ extend-ignore = [
119119

120120
[tool.ruff.lint.pydocstyle]
121121
convention = "pep257"
122+
123+
[tool.maturin]
124+
python-source = "src"
125+
module-name = "foamlib.foamlib_rust"

rust_src/lib.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use pyo3::prelude::*;
2+
3+
// Whitespace lookup table
4+
static IS_WHITESPACE: [bool; 256] = {
5+
let mut table = [false; 256];
6+
table[b' ' as usize] = true;
7+
table[b'\n' as usize] = true;
8+
table[b'\t' as usize] = true;
9+
table[b'\r' as usize] = true;
10+
table[b'\x0C' as usize] = true; // \f
11+
table[b'\x0B' as usize] = true; // \v
12+
table
13+
};
14+
15+
// Whitespace without newline lookup table
16+
static IS_WHITESPACE_NO_NEWLINE: [bool; 256] = {
17+
let mut table = [false; 256];
18+
table[b' ' as usize] = true;
19+
table[b'\t' as usize] = true;
20+
table[b'\r' as usize] = true;
21+
table[b'\x0C' as usize] = true; // \f
22+
table[b'\x0B' as usize] = true; // \v
23+
table
24+
};
25+
26+
/// Skip whitespace and comments in OpenFOAM files
27+
///
28+
/// This function skips over whitespace and comments (both // and /* */ style).
29+
/// It handles line continuations with backslash in line comments.
30+
///
31+
/// # Arguments
32+
/// * `contents` - The file contents as bytes or bytearray
33+
/// * `pos` - Current position in the file
34+
/// * `newline_ok` - Whether newlines should be skipped (default: True)
35+
///
36+
/// # Returns
37+
/// The new position after skipping whitespace and comments
38+
#[pyfunction]
39+
#[pyo3(signature = (contents, pos, *, newline_ok=true))]
40+
fn skip(contents: &Bound<'_, pyo3::types::PyAny>, mut pos: usize, newline_ok: bool) -> PyResult<usize> {
41+
// Extract bytes from either bytes or bytearray
42+
let contents = if let Ok(bytes) = contents.downcast::<pyo3::types::PyBytes>() {
43+
bytes.as_bytes()
44+
} else if let Ok(bytearray) = contents.downcast::<pyo3::types::PyByteArray>() {
45+
unsafe { bytearray.as_bytes() }
46+
} else {
47+
return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
48+
"contents must be bytes or bytearray"
49+
));
50+
};
51+
let is_whitespace = if newline_ok {
52+
&IS_WHITESPACE
53+
} else {
54+
&IS_WHITESPACE_NO_NEWLINE
55+
};
56+
57+
loop {
58+
// Skip whitespace
59+
while pos < contents.len() && is_whitespace[contents[pos] as usize] {
60+
pos += 1;
61+
}
62+
63+
// Check if we're at the end of content
64+
if pos >= contents.len() {
65+
break;
66+
}
67+
68+
// Check for comments
69+
if pos + 1 < contents.len() {
70+
let next1 = contents[pos];
71+
let next2 = contents[pos + 1];
72+
73+
// Single-line comment //
74+
if next1 == b'/' && next2 == b'/' {
75+
pos += 2;
76+
loop {
77+
if pos >= contents.len() {
78+
break;
79+
}
80+
81+
if contents[pos] == b'\n' {
82+
if newline_ok {
83+
pos += 1;
84+
}
85+
break;
86+
}
87+
88+
// Handle line continuation
89+
if contents[pos] == b'\\' && pos + 1 < contents.len() && contents[pos + 1] == b'\n' {
90+
pos += 1;
91+
}
92+
93+
pos += 1;
94+
}
95+
continue;
96+
}
97+
98+
// Multi-line comment /* */
99+
if next1 == b'/' && next2 == b'*' {
100+
pos += 2;
101+
102+
// Find the closing */
103+
let mut found = false;
104+
while pos + 1 < contents.len() {
105+
if contents[pos] == b'*' && contents[pos + 1] == b'/' {
106+
pos += 2;
107+
found = true;
108+
break;
109+
}
110+
pos += 1;
111+
}
112+
113+
if !found {
114+
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
115+
format!("Unterminated comment at position {}", pos)
116+
));
117+
}
118+
119+
continue;
120+
}
121+
}
122+
123+
// No more whitespace or comments
124+
break;
125+
}
126+
127+
Ok(pos)
128+
}
129+
130+
/// A Python module implemented in Rust for performance-critical parsing operations.
131+
#[pymodule]
132+
fn foamlib_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
133+
m.add_function(wrap_pyfunction!(skip, m)?)?;
134+
Ok(())
135+
}

src/foamlib/_files/_parsing/_parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
from typing_extensions import Unpack, assert_never
1414

1515
import numpy as np
16+
17+
# Import the Rust implementation of _skip
18+
try:
19+
from foamlib.foamlib_rust import skip as _skip_rust
20+
_USE_RUST_SKIP = True
21+
except ImportError:
22+
_USE_RUST_SKIP = False
1623
from multicollections import MultiDict
1724

1825
from ...typing import (
@@ -84,6 +91,19 @@ def _skip(
8491
*,
8592
newline_ok: bool = True,
8693
) -> int:
94+
# Use Rust implementation if available
95+
if _USE_RUST_SKIP:
96+
try:
97+
return _skip_rust(contents, pos, newline_ok=newline_ok)
98+
except ValueError as e:
99+
# Convert ValueError from Rust to FoamFileDecodeError
100+
raise FoamFileDecodeError(
101+
contents,
102+
len(contents),
103+
expected="*/",
104+
) from e
105+
106+
# Fallback to Python implementation
87107
is_whitespace = _IS_WHITESPACE if newline_ok else _IS_WHITESPACE_NO_NEWLINE
88108

89109
with contextlib.suppress(IndexError):

0 commit comments

Comments
 (0)