Skip to content
Merged
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ test-log = "0.2"
thiserror = "2.0"
tokio = "1.47.1"
urlencoding = "2.1.3"
line-index = "0.1.1"

[profile.dev]
panic = "unwind"
Expand Down
2 changes: 1 addition & 1 deletion nemo-language-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ tokio = ["dep:tokio"]

[dependencies]
anyhow = "1.0"
line-index = "0.1.1"
nemo = { path = "../nemo", default-features = false }
futures = { workspace = true }
strum = { workspace = true }
Expand All @@ -31,3 +30,4 @@ tokio = { workspace = true, features = ["macros", "io-util", "rt-multi-thread",
tower-lsp = { version = "0.20.0" }
tower-service = "0.3.2"
lsp-document = "0.6.0"
line-index = { workspace = true }
16 changes: 9 additions & 7 deletions nemo-python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{collections::HashSet, fs::read_to_string, time::Duration};

use nemo::{
api::load_program,
api::load_program_handle,
datavalues::{AnyDataValue, DataValue},
error::Error,
execution::{
Expand All @@ -17,7 +17,7 @@ use nemo::{
tag::Tag,
term::{Term, primitive::Primitive},
},
programs::ProgramRead,
programs::{ProgramRead, handle::ProgramHandle},
substitution::Substitution,
},
};
Expand Down Expand Up @@ -52,9 +52,10 @@ impl<T> PythonResult for (T, Vec<Error>) {
}
}

#[pyclass(from_py_object)]
/// A parsed and transformed nemo program.
#[pyclass(from_py_object, unsendable)]
#[derive(Clone)]
struct NemoProgram(nemo::rule_model::programs::program::Program);
struct NemoProgram(ProgramHandle);

#[pyfunction]
fn load_file(file: String) -> PyResult<NemoProgram> {
Expand All @@ -64,11 +65,11 @@ fn load_file(file: String) -> PyResult<NemoProgram> {

#[pyfunction]
fn load_string(rules: String) -> PyResult<NemoProgram> {
let program = load_program(rules, String::default())
let handle = load_program_handle(rules, String::default())
.map_err(Error::ProgramReport)
.py_res()?;

Ok(NemoProgram(program))
Ok(NemoProgram(handle))
}

#[pymethods]
Expand Down Expand Up @@ -411,8 +412,9 @@ impl NemoEngine {
.build()?;

let execution_parameters = ExecutionParameters::default();

let engine = rt
.block_on(ExecutionEngine::from_program(
.block_on(ExecutionEngine::from_handle(
program.0,
execution_parameters,
))
Expand Down
12 changes: 12 additions & 0 deletions nemo-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,18 @@ impl NemoEngine {
js_set
}

#[wasm_bindgen(js_name = "getLineNumberFromRuleId")]
pub fn line_number_from_rule_id(&self, rule_id: usize) -> Option<u32> {
let prog_handle = self.engine.original_program_handle();

let rule = prog_handle.rule_by_index(rule_id)?;
let position = prog_handle
.source_position(rule)
.expect("rule originates from the source");

Some(position.line)
}

#[wasm_bindgen]
pub async fn reason(&mut self) -> Result<(), NemoError> {
self.engine
Expand Down
1 change: 1 addition & 0 deletions nemo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ delegate = { workspace = true }
orx-imp-vec = "2.14"
itertools = { workspace = true }
indexmap = { workspace = true }
line-index = { workspace = true }

[dev-dependencies]
env_logger = { workspace = true }
Expand Down
24 changes: 21 additions & 3 deletions nemo/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,42 @@ pub async fn load_string(input: String) -> Result<Engine, Error> {
.into_object())
}

/// Parse a program in the given `input`-string and return a [Program].
/// Parse a program in the given `input`-string and return a [ProgramHandle]
/// for the transformed program.
///
/// In contrast to [load_program], this keeps the program's pipeline intact, so
/// the returned handle can be used to build an [Engine] (sharing a single
/// pipeline across parsing, transformation, execution and tracing) and to
/// resolve component origins back to the original program.
///
/// # Error
/// Returns a [ProgramReport] if parsing or validation fails.
pub fn load_program(input: String, label: String) -> Result<Program, ProgramReport> {
pub fn load_program_handle(input: String, label: String) -> Result<ProgramHandle, ProgramReport> {
let file = RuleFile::new(input, label);
let parameters = ExecutionParameters::default();

let handle = ProgramHandle::from_file(&file);
let report = ProgramReport::new(file);

let (program, report) = report.merge_program_parser_report(handle)?;

let (program, report) = report.merge_validation_report(
&program,
program.transform(TransformationDefault::new(&parameters)),
)?;

report.result(program.materialize())
report.result(program)
}

/// Parse a program in the given `input`-string and return a [Program].
///
/// Note that the resulting [Program] is a standalone snapshot without a
/// pipeline; for tracing or building an [Engine], prefer [load_program_handle].
///
/// # Error
/// Returns a [ProgramReport] if parsing or validation fails.
pub fn load_program(input: String, label: String) -> Result<Program, ProgramReport> {
load_program_handle(input, label).map(|program| program.materialize())
}

/// Validate the `input` and create a [ProgramReport] for error reporting
Expand Down
84 changes: 66 additions & 18 deletions nemo/src/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ use crate::{
io::{formats::Export, import_manager::ImportManager},
rule_file::RuleFile,
rule_model::{
components::tag::Tag,
pipeline::transformations::{default::TransformationDefault, global::TransformationGlobal},
programs::{handle::ProgramHandle, program::Program},
components::tag::Tag, pipeline::transformations::default::TransformationDefault,
programs::handle::ProgramHandle,
},
table_manager::{MemoryUsage, TableManager},
};

use super::{
execution_parameters::ExecutionParameters, selection_strategy::strategy::RuleSelectionStrategy,
tracing::rule_translation::RuleIdTranslation,
};

pub mod tracing;
Expand Down Expand Up @@ -58,6 +58,10 @@ pub struct ExecutionEngine<RuleSelectionStrategy> {
/// Normalized program
program: NormalizedProgram,

/// Translation of rule ids between the normalized and the original program,
/// used for tracing
rule_translation: RuleIdTranslation,

/// The picked selection strategy for rules
selection_strategy: RuleSelectionStrategy,

Expand Down Expand Up @@ -92,25 +96,29 @@ impl<Strategy: RuleSelectionStrategy> ExecutionEngine<Strategy> {
let report = ProgramReport::new(file);

let (program, report) = report.merge_program_parser_report(handle)?;

let (program, report) = report.merge_validation_report(
&program,
program.transform(TransformationDefault::new(&parameters)),
)?;

let engine = Self::initialize(program, parameters.import_manager).await?;
let engine = Self::from_handle(program, parameters).await?;

report.warned(engine)
}

/// Initialize the [ExecutionEngine] starting from a [Program]
pub async fn from_program(
program: Program,
/// Initialize the [ExecutionEngine] from an already prepared [ProgramHandle]
/// (for example one obtained from [crate::api::load_program_handle]).
///
/// This performs **no** transformation.
/// This is the entry point for callers that prepare
/// the program themselves (e.g. applying their own transformations); the
/// common case of going from a source file to an engine is covered by
/// [Self::from_file], which prepares the program and then calls this.
pub async fn from_handle(
program: ProgramHandle,
parameters: ExecutionParameters,
) -> Result<Self, Error> {
let program = ProgramHandle::from(program)
.transform(TransformationGlobal::new(&parameters.global_variables))
.expect("TransformationGlobal does not introduce validation errors");

Self::initialize(program, parameters.import_manager).await
}

Expand All @@ -120,6 +128,7 @@ impl<Strategy: RuleSelectionStrategy> ExecutionEngine<Strategy> {
import_manager: ImportManager,
) -> Result<Self, Error> {
let normalized_program = NormalizedProgram::normalize_program(&program_handle);
let rule_translation = RuleIdTranslation::new(&program_handle, &normalized_program);

let mut table_manager = TableManager::new();
Self::register_all_predicates(&mut table_manager, &normalized_program);
Expand All @@ -137,6 +146,7 @@ impl<Strategy: RuleSelectionStrategy> ExecutionEngine<Strategy> {
Ok(Self {
program_handle,
program: normalized_program,
rule_translation,
selection_strategy,
table_manager,
import_manager,
Expand Down Expand Up @@ -339,6 +349,20 @@ impl<Strategy: RuleSelectionStrategy> ExecutionEngine<Strategy> {
&self.program
}

/// Return the current [ProgramHandle].
///
/// Note that this represents the transformed program and not the original.
/// To obtain the original program, use [Self::original_program_handle].
pub fn current_program_handle(&self) -> ProgramHandle {
self.program_handle.clone()
}

/// Return the [ProgramHandle] of the original program, i.e. the program as
/// it was created from the user input (the first revision of the pipeline).
pub fn original_program_handle(&self) -> ProgramHandle {
self.program_handle.original_revision()
}

/// Creates an [Iterator] over all facts of a predicate.
pub async fn predicate_rows(
&mut self,
Expand Down Expand Up @@ -444,24 +468,48 @@ mod test {
use tokio;

use crate::{
api::load_program,
execution::{DefaultExecutionEngine, execution_parameters::ExecutionParameters},
rule_file::RuleFile,
};

#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn rule_source_position_reports_line() {
// A fact on line 1 and a rule on line 2.
let source = "a(1).
b(?x) :- a(?x).";

let file = RuleFile::new(source.to_string(), "test".to_string());

let engine = DefaultExecutionEngine::from_file(file, ExecutionParameters::default())
.await
.unwrap()
.into_object();

// The frontend resolves a tracing rule id via the original program handle.
let original = engine.original_program_handle();

// Rule id 0 is the first (and only) rule, which is on line 2, column 1.
let rule = original.rule_by_index(0).expect("rule id is valid");
let position = original
.source_position(rule)
.expect("rule originates from the source");
assert_eq!(position.line, 2);
assert_eq!(position.column, 1);
}

#[tokio::test]
#[test_log::test]
#[cfg_attr(miri, ignore)]
async fn issue_759() {
const ITERATIONS: usize = 32_768;

let program = load_program("foo(bar).".to_string(), Default::default()).unwrap();
let file = RuleFile::new("foo(bar).".to_string(), Default::default());

for _ in 1..=ITERATIONS {
let engine = DefaultExecutionEngine::from_program(
program.clone(),
ExecutionParameters::default(),
)
.await;
let engine =
DefaultExecutionEngine::from_file(file.clone(), ExecutionParameters::default())
.await;
assert_matches!(engine, Ok(_));
}
}
Expand Down
Loading