Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ae0731e
Implement Eldritch v2 Language Server Protocol (LSP)
google-labs-jules[bot] Dec 24, 2025
8334218
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
415e736
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
76adfa5
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
8966aea
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
0f2ef82
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
11459ee
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
cbf7813
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
f545205
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
c862288
Implement Eldritch v2 LSP and VS Code Integration
google-labs-jules[bot] Dec 24, 2025
6e4505a
feat: Add Eldritch LSP and VS Code extension support
google-labs-jules[bot] Dec 24, 2025
4fa4516
Merge branch 'main' into eldritch-lsp-v2-8861086955046603492
KCarretto Dec 24, 2025
263d849
style: Apply cargo fmt
google-labs-jules[bot] Dec 24, 2025
7c0431e
Implement LSP type checking for method calls and binary operations (#…
google-labs-jules[bot] Dec 24, 2025
16f483d
Fix unstable let expressions in eldritch-macros (#1401)
google-labs-jules[bot] Dec 24, 2025
6f0c2a7
feat(eldritch-lsp): add UndefinedSymbolRule linter rule (#1402)
google-labs-jules[bot] Dec 24, 2025
fb397a0
stuff
KCarretto Dec 24, 2025
a0c0714
stuff
KCarretto Dec 24, 2025
4c7de25
Update input_params highlighting and LSP linting (#1403)
google-labs-jules[bot] Dec 24, 2025
fdd5fbf
Fix syntax highlighting for Eldritch built-in and user-defined method…
google-labs-jules[bot] Dec 25, 2025
acd0ab1
Update VSCode plugin to show method signatures and doc blocks on hove…
google-labs-jules[bot] Dec 26, 2025
9151b64
Merge branch 'main' into eldritch-lsp-v2-8861086955046603492
KCarretto Dec 26, 2025
e39eefd
fix version
KCarretto Dec 26, 2025
d09a61e
slight fixes
KCarretto Dec 26, 2025
db7579b
Refactor linter rules into separate files (#1411)
google-labs-jules[bot] Dec 26, 2025
e228dc1
Refactor DeprecationRule to be metadata-driven via macros (#1412)
google-labs-jules[bot] Dec 26, 2025
14b7052
fix lsp
KCarretto Dec 26, 2025
d48fa6c
Fix clippy warnings and linter errors in eldritch-core and eldritch-l…
google-labs-jules[bot] Dec 26, 2025
e9d2639
Fix LSP completion regression by adding resilient lexing fallback (#1…
google-labs-jules[bot] Dec 26, 2025
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
107 changes: 2 additions & 105 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,51 +24,23 @@ jobs:
with:
go-version-file: go.mod
cache: true
- name: 🔨 Install Tools
run: |
# Pin gotestsum to v1.12.0 to support Go 1.23.4 (v1.13.0 requires Go 1.24)
go install gotest.tools/gotestsum@v1.12.0
go install github.com/ctrf-io/go-ctrf-json-reporter/cmd/go-ctrf-json-reporter@latest
- name: 🔨 Build
run: go build -v -o ./build/tavern ./tavern
- name: 🔎 Test
# Use gotestsum for human-readable output to stdout, and capture JSON to file for reporting
shell: bash
run: |
$(go env GOPATH)/bin/gotestsum --jsonfile test-output.json -- \
-v -race -coverprofile='coverage.out' -covermode=atomic ./tavern/...
- name: 🔄 Convert to CTRF
if: always() # Run even if tests fail so we report the failures
shell: bash
run: |
if [ -f test-output.json ]; then
cat test-output.json | $(go env GOPATH)/bin/go-ctrf-json-reporter -o ctrf-tavern-${{ matrix.os }}.json
# Normalize timestamps to avoid 50+ year durations when merging with reports lacking timestamps
jq '.results.summary.stop = (.results.summary.stop - .results.summary.start) | .results.summary.start = 0' ctrf-tavern-${{ matrix.os }}.json > ctrf-tavern-${{ matrix.os }}.json.tmp && mv ctrf-tavern-${{ matrix.os }}.json.tmp ctrf-tavern-${{ matrix.os }}.json
else
echo "test-output.json not found, skipping CTRF conversion"
fi
run: go test -v -race -coverprofile='coverage.out' -covermode=atomic ./tavern/...
- name: 📶 Upload Coverage Results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.out
flags: tavern
name: tavern-coverage
- name: 📤 Upload CTRF Results
if: always()
uses: actions/upload-artifact@v4
with:
name: ctrf-tavern-${{ matrix.os }}
path: ctrf-tavern-${{ matrix.os }}.json

implants:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
env:
IMIX_SERVER_PUBKEY: "pR56vDJZb9b3BL3ZvCXIvgK0r2vCk7FiZ1RjeEhJVyU="
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
Expand Down Expand Up @@ -109,46 +81,18 @@ jobs:
uses: taiki-e/install-action@v2
with:
tool: nextest,cargo-llvm-cov
- name: ⚡ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 🔎 Run tests
# nextest configured via nextest.toml to output junit.xml
run: |
cd ./implants/ &&
cargo fmt --check &&
cargo llvm-cov nextest --lcov --output-path lcov.info
- name: 🔄 Convert to CTRF
if: always()
shell: bash # Explicitly use bash to support standard shell syntax on Windows
run: |
cd ./implants/
# Move junit report if it exists
JUNIT_PATH=$(find . -name junit.xml | head -n 1)
if [ -n "$JUNIT_PATH" ]; then
mv "$JUNIT_PATH" junit-implants-${{ matrix.os }}.xml
else
echo "No JUnit report generated"
fi

if [ -f junit-implants-${{ matrix.os }}.xml ]; then
npx junit-to-ctrf junit-implants-${{ matrix.os }}.xml -o ctrf-implants-${{ matrix.os }}.json
fi
- name: 📶 Upload Coverage Results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./implants/lcov.info
flags: implants
name: implants-coverage-${{ matrix.os }}
- name: 📤 Upload CTRF Results
if: always()
uses: actions/upload-artifact@v4
with:
name: ctrf-implants-${{ matrix.os }}
path: implants/ctrf-implants-${{ matrix.os }}.json

ui-tests:
runs-on: ${{ matrix.os }}
strategy:
Expand All @@ -169,11 +113,7 @@ jobs:
- name: 📦 Install dependencies
run: npm ci
- name: 🔎 Run vitest with coverage
# Generate JUnit XML
run: npm test -- --run --coverage --reporter=junit --output-file=junit-ui-${{ matrix.os }}.xml
- name: 🔄 Convert to CTRF
if: always()
run: npx junit-to-ctrf junit-ui-${{ matrix.os }}.xml -o ctrf-ui-${{ matrix.os }}.json
run: npm test -- --run --coverage
- name: 📶 Upload Coverage Results
if: always()
uses: codecov/codecov-action@v5
Expand All @@ -182,46 +122,3 @@ jobs:
files: ./tavern/internal/www/coverage/lcov.info
flags: ui-tests
name: ui-tests-coverage
- name: 📤 Upload CTRF Results
if: always()
uses: actions/upload-artifact@v4
with:
name: ctrf-ui-${{ matrix.os }}
path: tavern/internal/www/ctrf-ui-${{ matrix.os }}.json

flaky-monitor:
needs: [tavern, implants, ui-tests]
runs-on: ubuntu-latest
if: always()
steps:
- name: 📥 Download CTRF Results
uses: actions/download-artifact@v4
with:
pattern: ctrf-*
path: ctrf-reports
merge-multiple: true

- name: 📊 Publish Test Report
uses: ctrf-io/github-test-reporter@v1
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
report-path: 'ctrf-reports/*.json'
upload-artifact: true
fetch-previous-results: true
pull-request: true
overwrite-comment: true

# Report Config
# https://github.com/ctrf-io/github-test-reporter/blob/main/docs/report-showcase.md
summary-delta-report: true
tests-changed-report: true
insights-report: true
slowest-report: true
failed-folded-report: true
flaky-rate-report: true
fail-rate-report: true
previous-results-report: true
previous-results-max: 100
artifact-name: 'flaky-report'
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,3 @@ implants/golem/embed_files_golem_prod/*

.venv
*.tfvars
node_modules/
4 changes: 0 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ Welcome to our repository! Most commands need to be run from the root of the pro
* Our user interface is located in `tavern/internal/www` and we managed dependencies within that directory using `npm`
* `implants/` contains Rust code that is deployed to target machines, such as our agent located in `implants/imix`.

## Rust

The majority of our rust codebase is in the `implants/` directory & workspaces. When adding dependencies to crates within this workspace, please add them to the workspace root and have the crate use the workspace dependency, in order to centralize version management. ALWAYS RUN `cargo fmt` WHEN MAKING RUST CHANGES, OR CI WILL FAIL.

## Golang Tests

To run all Golang tests in our repository, please run `go test ./...` from the project root.
Expand Down
2 changes: 0 additions & 2 deletions implants/.config/nextest.toml

This file was deleted.

1 change: 1 addition & 0 deletions implants/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"lib/eldritchv2/eldritch-macros",
"lib/eldritchv2/eldritch-repl",
"lib/eldritchv2/eldritch-agent",
"lib/eldritchv2/eldritch-lsp",
"lib/eldritchv2/stdlib/eldritch-libagent",
"lib/eldritchv2/stdlib/eldritch-libassets",
"lib/eldritchv2/stdlib/eldritch-libcrypto",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn bench_function_call(c: &mut Criterion) {
b.iter(|| {
let mut interpreter = Interpreter::new();
let code = "
def add(a, b):
fn add(a, b):
return a + b

add(5, 10)
Expand Down
4 changes: 4 additions & 0 deletions implants/lib/eldritchv2/eldritch-core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use spin::RwLock;

// Resolve circular reference for ForeignValue signature
use crate::interpreter::Interpreter;
use crate::interpreter::signature::MethodSignature;

#[derive(Debug)]
pub struct Environment {
Expand Down Expand Up @@ -59,6 +60,9 @@ pub type BuiltinFnWithKwargs =
pub trait ForeignValue: fmt::Debug + Send + Sync {
fn type_name(&self) -> &str;
fn method_names(&self) -> Vec<String>;
fn get_method_signature(&self, _name: &str) -> Option<MethodSignature> {
None
}
fn call_method(
&self,
interp: &mut Interpreter,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::ast::{Environment, Value};
use crate::interpreter::introspection::get_type_name;
use alloc::format;
use alloc::string::String;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::char;
use spin::RwLock;
Expand Down
60 changes: 46 additions & 14 deletions implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,29 +351,42 @@ impl Interpreter {
let mut lexer = Lexer::new(line_up_to_cursor.to_string());
let tokens = match lexer.scan_tokens() {
Ok(t) => t,
// If scanning fails (e.g. open string), we might still want to try?
// For now, if lexer fails, we fallback to simple word splitting or empty.
// If scanning fails (e.g. due to syntax error earlier in the file),
// we try to be resilient and just scan the current line.
Err(_) => {
// Fallback: Check if we are typing a simple identifier at end
let last_word = line_up_to_cursor
.split_terminator(|c: char| !c.is_alphanumeric() && c != '_')
.next_back()
.unwrap_or("");
if !last_word.is_empty() {
prefix = last_word.to_string();
// Attempt to scan just the last line
let last_line = line_up_to_cursor.lines().last().unwrap_or("");
let mut fallback_lexer = Lexer::new(last_line.to_string());
match fallback_lexer.scan_tokens() {
Ok(t) => t,
Err(_) => {
// Fallback: Check if we are typing a simple identifier at end
let last_word = line_up_to_cursor
.split_terminator(|c: char| !c.is_alphanumeric() && c != '_')
.next_back()
.unwrap_or("");
if !last_word.is_empty() {
prefix = last_word.to_string();
}
// If we can't tokenize, we assume we can't do dot-access analysis safely.
// But we can still try to suggest globals matching the prefix.
// We return empty tokens list, but allow execution to proceed to prefix filtering.
Vec::new()
}
}
// If we can't tokenize, we assume we can't do dot-access analysis safely.
// But we can still try to suggest globals matching the prefix.
// We return empty tokens list, but allow execution to proceed to prefix filtering.
Vec::new()
}
};

// Determine context from tokens
let mut target_val: Option<Value> = None;
let meaningful_tokens: Vec<&super::super::token::Token> = tokens
.iter()
.filter(|t| t.kind != TokenKind::Eof && t.kind != TokenKind::Newline)
.filter(|t| {
t.kind != TokenKind::Eof
&& t.kind != TokenKind::Newline
&& t.kind != TokenKind::Indent
&& t.kind != TokenKind::Dedent
})
.collect();

if !meaningful_tokens.is_empty() {
Expand Down Expand Up @@ -401,6 +414,19 @@ impl Interpreter {
TokenKind::String(s) => {
target_val = Some(Value::String(s.clone()));
}
TokenKind::RBracket => {
// Assume List
target_val = Some(Value::List(Arc::new(RwLock::new(Vec::new()))));
}
TokenKind::RBrace => {
// Assume Dictionary (could be Set, but Dict is safer default)
target_val =
Some(Value::Dictionary(Arc::new(RwLock::new(BTreeMap::new()))));
}
TokenKind::RParen => {
// Assume Tuple
target_val = Some(Value::Tuple(Vec::new()));
}
_ => {}
}
}
Expand Down Expand Up @@ -446,6 +472,12 @@ impl Interpreter {
match &val {
Value::Foreign(obj) => {
for m in obj.method_names() {
#[allow(clippy::collapsible_if)]
if let Some(sig) = obj.get_method_signature(&m) {
if sig.deprecated.is_some() {
continue;
}
}
candidates.insert(m);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,28 @@ pub(crate) fn evaluate_index(
}
Ok(Value::String(chars[true_idx as usize].to_string()))
}
Value::Bytes(b) => {
let idx_int = match idx_val {
Value::Int(i) => i,
_ => {
return interp.error(
EldritchErrorKind::TypeError,
"bytes indices must be integers",
index.span,
);
}
};
let len = b.len() as i64;
let true_idx = if idx_int < 0 { len + idx_int } else { idx_int };
if true_idx < 0 || true_idx as usize >= b.len() {
return interp.error(
EldritchErrorKind::IndexError,
"Bytes index out of range",
span,
);
}
Ok(Value::Int(b[true_idx as usize] as i64))
}
_ => interp.error(
EldritchErrorKind::TypeError,
&format!("'{}' object is not subscriptable", get_type_name(&obj_val)),
Expand Down Expand Up @@ -249,6 +271,28 @@ pub(crate) fn evaluate_slice(
}
Ok(Value::String(result_chars.into_iter().collect()))
}
Value::Bytes(b) => {
let len = b.len() as i64;
let (i, j) = adjust_slice_indices(len, &start_val_opt, &stop_val_opt, step_val);
let mut result_bytes = Vec::new();
let mut curr = i;
if step_val > 0 {
while curr < j {
if curr >= 0 && curr < len {
result_bytes.push(b[curr as usize]);
}
curr += step_val;
}
} else {
while curr > j {
if curr >= 0 && curr < len {
result_bytes.push(b[curr as usize]);
}
curr += step_val;
}
}
Ok(Value::Bytes(result_bytes))
}
_ => interp.error(
EldritchErrorKind::TypeError,
&format!("'{}' object is not subscriptable", get_type_name(&obj_val)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use super::utils::parse_error_kind;
use super::{MAX_RECURSION_DEPTH, evaluate};
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::format;
use alloc::string::ToString;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use spin::RwLock;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::ArgCheck;
use crate::ast::Value;
use alloc::collections::BTreeMap;
use alloc::format;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
Expand Down
Loading
Loading