Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ jobs:
- name: Lint (clippy, blocking)
if: matrix.os == 'ubuntu-latest'
run: cargo clippy --all-targets -- -D warnings
- name: Pure-Rust core (no python feature, consumed by rtl-lsp)
if: matrix.os == 'ubuntu-latest'
run: |
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test --no-default-features
- name: Build and install
run: |
pip install maturin pytest
Expand Down
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ license = "MIT"
name = "pyregtab"
crate-type = ["cdylib", "rlib"]

[features]
# The Python bindings (pyo3 module, External/Custom callbacks). Disable with
# `--no-default-features` to build the pure-Rust core (RTL compiler, matcher,
# interpreter) for embedding, e.g. in rtl-lsp.
default = ["python"]
python = ["dep:pyo3"]

[dependencies]
pyo3 = { version = "0.23", features = ["abi3-py310"] }
pyo3 = { version = "0.23", features = ["abi3-py310"], optional = true }
regex = "1"
indexmap = "2"

Expand Down
16 changes: 9 additions & 7 deletions src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@ use crate::syntax::SyntaxCore;
use crate::util::CoreResult;
use std::collections::HashMap;

#[pyo3::pyclass(eq, eq_int, name = "SchemaConstructionStrategy")]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int, name = "SchemaConstructionStrategy", rename_all = "SCREAMING_SNAKE_CASE"))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SchemaStrategy {
#[pyo3(name = "RECORD_FIRST")]
RecordFirst,
#[pyo3(name = "POSITION_FIRST")]
PositionFirst,
}

#[pyo3::pyclass(eq, eq_int, name = "ActionApplicationStrategy")]
#[cfg_attr(feature = "python", pyo3::pyclass(eq, eq_int, name = "ActionApplicationStrategy", rename_all = "SCREAMING_SNAKE_CASE"))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ActionStrategy {
#[pyo3(name = "ROW_FIRST")]
RowFirst,
#[pyo3(name = "COLUMN_FIRST")]
ColumnFirst,
}

Expand Down Expand Up @@ -49,7 +45,7 @@ pub fn interpret(
cfg: &InterpreterCfg,
syntax: &SyntaxCore,
sem: &SemanticsCore,
py_table: Option<&pyo3::Py<pyo3::PyAny>>,
py_table: Option<&crate::spec::PyTableHandle>,
) -> CoreResult<RecordsetCore> {
let env = EvalEnv { syntax, py_table };

Expand Down Expand Up @@ -318,6 +314,12 @@ fn generate_records(
fn handle_missing(cfg: &InterpreterCfg, attribute: &str) -> CoreResult<Option<String>> {
match &cfg.missing_value_handler {
None => Ok(None),
#[cfg(feature = "python")]
Some(f) => crate::py::call_missing_handler(f, attribute),
#[cfg(not(feature = "python"))]
Some(f) => {
let _ = attribute;
match *f {}
}
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pub mod interp;
pub mod matcher;
#[cfg(feature = "python")]
pub mod py;
pub mod recordset;
pub mod rtl;
Expand All @@ -13,8 +14,10 @@ pub mod syntax;
mod tests;
pub mod util;

#[cfg(feature = "python")]
use pyo3::prelude::*;

#[cfg(feature = "python")]
#[pymodule]
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
// syntax
Expand Down
12 changes: 11 additions & 1 deletion src/py.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ fn rtl_err(e: RtlErr) -> PyErr {
} else {
format!("RTL compile error: {}", e.msg)
};
RtlCompileError::new_err(msg)
let err = RtlCompileError::new_err(msg);
// Expose the source position programmatically: err.line (1-based) and
// err.col (0-based), both None when unknown.
Python::with_gil(|py| {
let value = err.value(py);
let line = if e.line >= 0 { Some(e.line) } else { None };
let col = if e.col >= 0 { Some(e.col) } else { None };
let _ = value.setattr("line", line);
let _ = value.setattr("col", col);
});
err
}

// ================================================================ callbacks
Expand Down
20 changes: 20 additions & 0 deletions src/rtl/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use std::sync::Arc;

pub struct ATPBuilder<'a> {
bindings: &'a BindingsCore,
/// Permissive mode: an `EXT('name')` with no binding becomes an
/// always-true `ExternalUnbound` stub instead of a compile error
/// (standalone editing without a host runtime, e.g. rtl-lsp).
permissive: bool,
inherited_stack: Vec<Vec<ActionSpec>>,
cell_frags: HashMap<String, Option<CellBodyAst>>,
row_frags: HashMap<String, RowBodyAst>,
Expand Down Expand Up @@ -45,8 +49,13 @@ fn build_extractor(steps: &[PStep]) -> Extractor {

impl<'a> ATPBuilder<'a> {
pub fn new(bindings: &'a BindingsCore) -> Self {
Self::with_permissive(bindings, false)
}

pub fn with_permissive(bindings: &'a BindingsCore, permissive: bool) -> Self {
ATPBuilder {
bindings,
permissive,
inherited_stack: Vec::new(),
cell_frags: HashMap::new(),
row_frags: HashMap::new(),
Expand Down Expand Up @@ -647,7 +656,13 @@ impl<'a> ATPBuilder<'a> {
));
}
match self.bindings.filter.get(name) {
// Unreachable without the `python` feature: PyFunc is
// uninhabited there, so the map is always empty.
#[allow(unreachable_code)]
Some(f) => FilterTerm::External { name: name.clone(), func: f.clone() },
None if self.permissive => {
FilterTerm::ExternalUnbound { name: name.clone() }
}
None => {
let hint = if self.bindings.cell.contains_key(name) {
format!(
Expand Down Expand Up @@ -690,7 +705,12 @@ impl<'a> ATPBuilder<'a> {
));
}
match self.bindings.cell.get(name) {
// Unreachable without the `python` feature (see above).
#[allow(unreachable_code)]
Some(f) => CellPredicate::External { name: name.clone(), func: f.clone() },
None if self.permissive => {
CellPredicate::ExternalUnbound { name: name.clone() }
}
None => {
let hint = if self.bindings.filter.contains_key(name) {
format!(
Expand Down
19 changes: 18 additions & 1 deletion src/rtl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ impl From<CoreErr> for RtlErr {
fn from(e: CoreErr) -> Self {
match e {
CoreErr::Msg(m) => RtlErr::new(m),
#[cfg(feature = "python")]
CoreErr::Py(e) => RtlErr::new(format!("{e}")),
}
}
Expand All @@ -46,6 +47,22 @@ pub struct BindingsCore {

/// Port of `RtlCompiler.compile(rtl, bindings)`.
pub fn compile(rtl: &str, bindings: &BindingsCore) -> Result<TablePattern, RtlErr> {
compile_inner(rtl, bindings, false)
}

/// Permissive compilation for standalone editing (rtl-lsp): any `EXT('name')`
/// is considered bound via an always-true stub, so `.rtl` files that rely on
/// host-runtime `Bindings` do not produce false errors. Everything else is
/// checked exactly as in [`compile`].
pub fn compile_permissive(rtl: &str) -> Result<TablePattern, RtlErr> {
compile_inner(rtl, &BindingsCore::default(), true)
}

fn compile_inner(
rtl: &str,
bindings: &BindingsCore,
permissive: bool,
) -> Result<TablePattern, RtlErr> {
let tokens = lexer::lex(rtl)?;
let tree = parser::parse(tokens)?;

Expand Down Expand Up @@ -121,7 +138,7 @@ pub fn compile(rtl: &str, bindings: &BindingsCore) -> Result<TablePattern, RtlEr
}
}

let pattern = build::ATPBuilder::new(bindings).build(&tree)?;
let pattern = build::ATPBuilder::with_permissive(bindings, permissive).build(&tree)?;
if transforms.is_empty() {
Ok(pattern)
} else {
Expand Down
Loading
Loading