"Pixel" is the custom query and scripting language of SEMOSS. It is an imperative and dataflow-oriented language designed to allow users and developers to express a wide array of operations within the SEMOSS ecosystem. These operations include data querying and transformation, workflow automation, and interaction with engines such as databases, models, and functions.
The src/prerna/sablecc2/ package and its sub-packages are responsible for parsing, translating, and executing these Pixel scripts. The name sablecc2 suggests that the SableCC parser generator tool was used to create the underlying parsing infrastructure, which is evident from the presence of files like lexer.dat, parser.dat, and the typical structure of sub-packages (lexer/, parser/, node/, analysis/).
- Data Access and Manipulation: Querying data from various engines (databases, local frames, etc.), filtering, joining, aggregating, and transforming data.
- Workflow Orchestration: Chaining multiple operations together to create complex data processing pipelines or analytical workflows.
- Engine Interaction: Sending commands to and receiving data from different types of
IEngineimplementations (databases, AI models, storage, functions). - Variable Management: Creating and using variables to store intermediate results, parameters, or references to data frames.
- Extensibility: Providing a common language to invoke modular units of logic called "Reactors".
Pixel scripts consist of one or more statements, typically separated by semicolons. Each statement usually involves a Reactor invocation or a variable assignment.
-
Reactor Invocation:
- The most common operation is calling a Reactor. Reactors are named functions that perform specific tasks.
- Syntax:
ReactorName(paramKey1=[value1], paramKey2=[value2], ...); - Example:
RunQuery(engine=["myPostgresDb"], query=["SELECT * FROM my_table"]); - Parameters are passed as key-value pairs, where the value is often a list (even for single values).
-
Variable Assignment:
- Results from Reactor calls or literal values can be assigned to variables.
- Syntax:
variableName = ... ; - Variables are stored in the current
Insight'sVarStore. - Examples:
myAge = 25; greeting = "Hello, SEMOSS!"; myNumbers = [1, 2, 3, 4, 5]; countryCapitals = {"USA":"Washington D.C.", "Canada":"Ottawa"}; customerFrame = Database(id=["salesDb"]) | Select(columns=["CustomerID", "Name", "Region"]);
-
Chaining Operations (Piping):
- The pipe operator
|is used to chain operations, where the output of one operation becomes an implicit input to the next. This is fundamental to Pixel's dataflow nature. - The output of the preceding operation is often implicitly passed as the primary input (or context) to the subsequent Reactor.
- Example:
myFrame = Database(id=["salesDb"]) | Select(columns=["OrderDate", "Product", "SalesAmount"]); filteredFrame = Frame("myFrame") | Filter(column=["Product"], comparator=["=="], value=["Laptop"]); groupedSales = Frame("filteredFrame") | GroupBy(columns=["Product"], aggregations=[{"SalesAmount":"SUM"}]);
- The pipe operator
-
Referencing Variables:
- Variables can be referenced in subsequent operations using the
$prefix. - Example:
productName = "Laptop"; filteredFrame = Frame("myFrame") | Filter(column=["Product"], comparator=["=="], value=[$productName]);
- Variables can be referenced in subsequent operations using the
-
Comments:
- Pixel supports comments using
//for single-line comments or/* ... */for multi-line comments. - Example:
// This is a single-line comment /* * This is a * multi-line comment. */ myVar = 10; // Assign 10 to myVar
- Pixel supports comments using
When providing literal values in Pixel scripts:
- Strings: Enclosed in double quotes (e.g.,
"Hello World"). - Numbers: Integers (e.g.,
123) or decimals (e.g.,45.67). - Booleans:
trueorfalse. - Lists: Comma-separated values enclosed in square brackets (e.g.,
[1, "apple", true]). - Maps (Dictionaries): Key-value pairs enclosed in curly braces, with keys typically being strings (e.g.,
{"name":"John Doe", "age":30}).
These literals, when passed as parameters to Reactors, are wrapped into NounMetadata objects.
Pixel itself doesn't have extensive built-in control flow structures like loops (for, while) or complex branching in its core syntax in the same way a general-purpose programming language might. However, control flow is primarily achieved through:
- Conditional Reactors: Reactors like
IfReactorallow for conditional execution of other Pixel commands or assignment of values.- Example:
myValue = If(condition=[$someVar > 10], then=["High"], else=["Low"]);
- Example:
- Workflow Orchestration via Chaining: The sequential nature of Pixel scripts and the piping mechanism inherently define a flow of execution.
- Java-Implemented Logic: More complex looping or conditional logic is often handled within the Java code of a specific Reactor implementation.
- Parsing: The Pixel script string is parsed by
PixelRunnerusing the SableCC-generated lexer and parser into an Abstract Syntax Tree (AST). - Translation & Planning:
PixelRunnerusesGreedyTranslationwhich, in conjunction withPixelPlanner, traverses the AST. - Reactor Invocation: Operations in the Pixel script (like
Database(...),Select(...), assignments) are typically mapped to specificIReactorimplementations.ReactorFactorymight be involved in instantiating these reactors. - Execution Context (
Insight): All Pixel execution happens within the context of anInsightobject, which holds theVarStorefor variables and thePixelListfor tracking executed operations. - Result Handling: Reactors return
NounMetadata, which can be stored in variables or passed to other reactors.
This structure makes Pixel a flexible language for both interactive data exploration and for defining reusable, stored analytical recipes within SEMOSS.
The Pixel language serves as a user-friendly and scriptable interface to the underlying Reactor framework, which provides the actual logic for most operations in SEMOSS. Understanding their relationship is key to understanding SEMOSS's execution model.
-
Mapping Pixels to Reactors:
- Most non-trivial operations expressed in Pixel (e.g.,
Database(...),Select(...),Filter(...),RunLlm(...)) are designed to map directly or indirectly to a corresponding JavaIReactorimplementation. - The name of the function used in Pixel (e.g.,
Database,Select) often corresponds to a specific Reactor class (e.g.,DatabaseReactor,SelectReactor). Theprerna.reactor.ReactorFactoryclass is typically responsible for resolving a Pixel function name to its Java Reactor class. - For example, a Pixel command like
MyFrame = Database(id=["myEngine"]) | Select(columns=["Name", "Age"]);involves at least two reactors: one for connecting to the database (DatabaseReactor) and one for selecting columns (SelectReactor).
- Most non-trivial operations expressed in Pixel (e.g.,
-
Role of
PixelRunnerandPixelPlanner:- When a Pixel script is executed via
PixelRunner.runPixel(...):- The script is parsed into an Abstract Syntax Tree (AST).
- The
GreedyTranslationvisitor traverses this AST. - As
GreedyTranslationencounters nodes in the AST that represent Reactor calls (likeDatabase(...)orSelect(...)), it instantiates the appropriateIReactorobjects (often usingReactorFactory). - It populates the Reactor's
NounStorewith the parameters provided in the Pixel script. - The
PixelPlanner(which is managed byGreedyTranslation) plays a crucial role in orchestrating the execution of these Reactors. It:- Manages the overall execution plan, including the sequence of Reactor calls.
- Handles the data flow between chained Reactors (the
|pipe operator). The output (NounMetadata) of one Reactor in a chain often becomes an implicit input (the "current row" or context, accessible viagetCurRow()inAbstractReactor) for the next Reactor. - Manages the
VarStore(part of theInsight) for storing and retrieving variables (e.g., if a Reactor's output is assigned to a variable likemyFrame = ...). - May optimize the sequence of operations or combine steps where possible (e.g., merging multiple filter operations or pushing operations down to a database engine).
- When a Pixel script is executed via
-
Chaining and Data Flow:
- The pipe operator
|in Pixel is a powerful construct thatPixelPlannertranslates into a data flow pipeline between Reactors. - Example:
Frame("inputFrame") | Filter(...) | GroupBy(...);FrameReactor("inputFrame")is executed, likely loading a frame into context (perhaps making it thecurRowor a known variable inVarStore).FilterReactoris executed next. It implicitly operates on the output/context ofFrameReactor. Its parameters (filter conditions) are taken from the Pixel call. It produces a filtered frame or modifies the context.GroupByReactorthen operates on the output/context ofFilterReactor.
- The
PixelPlannerensures that the outputNounMetadatafrom one reactor is correctly made available as input to the next, either by setting it as the current data context or by resolving variable names specified as inputs in the Pixel parameters.
- The pipe operator
In essence, Pixel provides the syntax for defining workflows and operations, while Reactors provide the modular, executable logic. The PixelRunner and PixelPlanner act as the bridge, translating the user's Pixel script into a planned sequence of Reactor executions and managing the data flow between them within the context of an Insight.