Archived
be013d2b42
Per the consolidation onto El: 'crates' is the Rust word, 'vessel' is El's (per spec/language.md §15). The directory rename is the structural marker that this slot holds an El buildable unit, even if its current contents are still Rust pending port. Mechanical: git mv crates vessels, sed workspace members and any path dependencies, update CI workflow paths, update README references. Cross-repo path dependencies (`../foo/crates/bar`) updated workspace- wide so cargo metadata still resolves where the Rust still builds.
293 lines
13 KiB
Rust
293 lines
13 KiB
Rust
//! Completion engine — prefix-based + semantic scoring.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use el_types::{TypeDef, TypeEnv};
|
|
|
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Completion {
|
|
pub label: String,
|
|
pub kind: CompletionKind,
|
|
pub detail: String,
|
|
pub documentation: Option<String>,
|
|
/// Activation strength: higher = more relevant, completions sorted desc.
|
|
pub score: f32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum CompletionKind {
|
|
Variable,
|
|
Function,
|
|
Type,
|
|
Keyword,
|
|
}
|
|
|
|
// ── Keywords ──────────────────────────────────────────────────────────────────
|
|
|
|
const KEYWORDS: &[&str] = &[
|
|
"let", "fn", "type", "enum", "match", "return", "activate", "where",
|
|
"sealed", "if", "else", "for", "in", "while", "true", "false",
|
|
"test", "seed", "assert", "target", "protocol", "impl",
|
|
"import", "from", "as", "with", "retry", "times", "fallback",
|
|
"reason", "parallel", "trace", "requires", "deploy", "to", "via",
|
|
];
|
|
|
|
// ── Builtin functions ─────────────────────────────────────────────────────────
|
|
|
|
const BUILTIN_FUNCTIONS: &[(&str, &str)] = &[
|
|
("println", "fn(value: String) -> Void"),
|
|
("print", "fn(value: String) -> Void"),
|
|
("int_to_str", "fn(n: Int) -> String"),
|
|
("str_to_int", "fn(s: String) -> Int"),
|
|
("string_len", "fn(s: String) -> Int"),
|
|
("str_slice", "fn(s: String, start: Int, end: Int) -> String"),
|
|
("str_concat", "fn(a: String, b: String) -> String"),
|
|
("str_contains", "fn(s: String, sub: String) -> Bool"),
|
|
("str_starts_with", "fn(s: String, prefix: String) -> Bool"),
|
|
("str_ends_with", "fn(s: String, suffix: String) -> Bool"),
|
|
("str_to_upper", "fn(s: String) -> String"),
|
|
("str_to_lower", "fn(s: String) -> String"),
|
|
("str_split", "fn(s: String, sep: String) -> [String]"),
|
|
("str_trim", "fn(s: String) -> String"),
|
|
("str_replace", "fn(s: String, from: String, to: String) -> String"),
|
|
("str_index_of", "fn(s: String, sub: String) -> Int"),
|
|
("float_to_str", "fn(f: Float) -> String"),
|
|
("str_to_float", "fn(s: String) -> Float"),
|
|
("array_push", "fn(arr: [T], val: T) -> [T]"),
|
|
("array_len", "fn(arr: [T]) -> Int"),
|
|
("array_get", "fn(arr: [T], i: Int) -> T"),
|
|
("list_len", "fn(list: [T]) -> Int"),
|
|
("list_get", "fn(list: [T], i: Int) -> T"),
|
|
("list_map", "fn(list: [T], f: fn(T) -> U) -> [U]"),
|
|
("list_filter", "fn(list: [T], f: fn(T) -> Bool) -> [T]"),
|
|
("list_reduce", "fn(list: [T], init: U, f: fn(U, T) -> U) -> U"),
|
|
("map_create", "fn() -> Map"),
|
|
("map_set", "fn(m: Map, key: String, val: T) -> Map"),
|
|
("map_get", "fn(m: Map, key: String) -> T"),
|
|
("map_has", "fn(m: Map, key: String) -> Bool"),
|
|
("math_abs", "fn(n: Float) -> Float"),
|
|
("math_sqrt", "fn(n: Float) -> Float"),
|
|
("math_floor", "fn(n: Float) -> Int"),
|
|
("math_ceil", "fn(n: Float) -> Int"),
|
|
("math_round", "fn(n: Float) -> Int"),
|
|
("math_min", "fn(a: Float, b: Float) -> Float"),
|
|
("math_max", "fn(a: Float, b: Float) -> Float"),
|
|
("math_pow", "fn(base: Float, exp: Float) -> Float"),
|
|
("now_millis", "fn() -> Int"),
|
|
("time_now_utc", "fn() -> String"),
|
|
("time_to_parts", "fn(ts: String) -> Map"),
|
|
("time_format", "fn(ts: String, fmt: String) -> String"),
|
|
("llm_call", "fn(prompt: String) -> String"),
|
|
("llm_parallel", "fn(prompts: [String]) -> [String]"),
|
|
("random_int", "fn(min: Int, max: Int) -> Int"),
|
|
("random_float", "fn() -> Float"),
|
|
("parse_json", "fn(s: String) -> Map"),
|
|
("to_json", "fn(v: T) -> String"),
|
|
("http_get", "fn(url: String) -> String"),
|
|
("http_post", "fn(url: String, body: String) -> String"),
|
|
("read_file", "fn(path: String) -> String"),
|
|
("write_file", "fn(path: String, content: String) -> Void"),
|
|
("env_get", "fn(key: String) -> String"),
|
|
("sleep_ms", "fn(ms: Int) -> Void"),
|
|
("uuid_new", "fn() -> String"),
|
|
("hash_sha256", "fn(s: String) -> String"),
|
|
];
|
|
|
|
const BUILTIN_TYPES: &[(&str, &str)] = &[
|
|
("Int", "64-bit signed integer"),
|
|
("Float", "64-bit IEEE 754 double"),
|
|
("String", "UTF-8 string"),
|
|
("Bool", "Boolean value"),
|
|
("Uuid", "RFC 4122 UUID"),
|
|
("Void", "Unit type — no value"),
|
|
];
|
|
|
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
/// Produce completions at `cursor_pos` in `source`.
|
|
///
|
|
/// Extracts the identifier prefix before the cursor and returns all
|
|
/// completions whose label starts with that prefix, sorted by score
|
|
/// (prefix match score + semantic boost).
|
|
pub fn completions_at(env: &TypeEnv, source: &str, cursor_pos: usize) -> Vec<Completion> {
|
|
let prefix = extract_prefix(source, cursor_pos);
|
|
let mut results: Vec<Completion> = Vec::new();
|
|
|
|
// Keywords
|
|
for &kw in KEYWORDS {
|
|
if kw.starts_with(&prefix) {
|
|
let score = prefix_score(kw, &prefix)
|
|
+ if matches!(kw, "activate" | "sealed") { 0.2 } else { 0.0 };
|
|
results.push(Completion {
|
|
label: kw.to_string(),
|
|
kind: CompletionKind::Keyword,
|
|
detail: "keyword".into(),
|
|
documentation: keyword_doc(kw),
|
|
score,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Built-in types
|
|
for &(name, desc) in BUILTIN_TYPES {
|
|
if name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
|
|
results.push(Completion {
|
|
label: name.to_string(),
|
|
kind: CompletionKind::Type,
|
|
detail: desc.into(),
|
|
documentation: Some(format!("Built-in type: {desc}")),
|
|
score: prefix_score(name, &prefix) + 0.1,
|
|
});
|
|
}
|
|
}
|
|
|
|
// User-defined types from env
|
|
for (type_name, def) in &env.types {
|
|
// Skip built-ins already listed above
|
|
if BUILTIN_TYPES.iter().any(|(n, _)| *n == type_name) {
|
|
continue;
|
|
}
|
|
if type_name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
|
|
let (detail, doc, extra_score) = match def {
|
|
TypeDef::Struct { fields, .. } => {
|
|
let field_list = fields
|
|
.iter()
|
|
.map(|(f, t)| format!("{f}: {t}"))
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
(format!("struct {{ {field_list} }}"),
|
|
Some(format!("User-defined struct with fields: {field_list}")),
|
|
0.15)
|
|
}
|
|
TypeDef::Enum { variants, .. } => {
|
|
let v_list = variants.iter().map(|v| v.name.clone()).collect::<Vec<_>>().join(", ");
|
|
(format!("enum {{ {v_list} }}"),
|
|
Some(format!("Enum variants: {v_list}")),
|
|
0.15)
|
|
}
|
|
TypeDef::Primitive(_) => {
|
|
("primitive type".into(), None, 0.05)
|
|
}
|
|
TypeDef::Protocol { methods, .. } => {
|
|
let m_list = methods.iter().map(|m| m.name.clone()).collect::<Vec<_>>().join(", ");
|
|
(format!("protocol {{ {m_list} }}"),
|
|
Some(format!("Protocol methods: {m_list}")),
|
|
0.15)
|
|
}
|
|
};
|
|
results.push(Completion {
|
|
label: type_name.clone(),
|
|
kind: CompletionKind::Type,
|
|
detail,
|
|
documentation: doc,
|
|
score: prefix_score(type_name, &prefix) + extra_score,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Builtin functions
|
|
for &(name, sig) in BUILTIN_FUNCTIONS {
|
|
if name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
|
|
results.push(Completion {
|
|
label: name.to_string(),
|
|
kind: CompletionKind::Function,
|
|
detail: sig.into(),
|
|
documentation: Some(format!("Built-in function: {name}\n{sig}")),
|
|
score: prefix_score(name, &prefix) + 0.11,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Functions from env (user-defined)
|
|
for (fn_name, fn_type) in &env.functions {
|
|
if fn_name.to_lowercase().starts_with(&prefix.to_lowercase()) || prefix.is_empty() {
|
|
results.push(Completion {
|
|
label: fn_name.clone(),
|
|
kind: CompletionKind::Function,
|
|
detail: fn_type.to_string(),
|
|
documentation: Some(format!("Function: {fn_name} :: {fn_type}")),
|
|
score: prefix_score(fn_name, &prefix) + 0.12,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort by score descending, deduplicate by label
|
|
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
|
|
results.dedup_by(|a, b| a.label == b.label);
|
|
results
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
/// Extract the identifier fragment immediately before `cursor_pos`.
|
|
fn extract_prefix(source: &str, cursor_pos: usize) -> String {
|
|
let end = cursor_pos.min(source.len());
|
|
let bytes = source.as_bytes();
|
|
let mut start = end;
|
|
while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') {
|
|
start -= 1;
|
|
}
|
|
source[start..end].to_string()
|
|
}
|
|
|
|
/// Score based on how much of the label is matched by the prefix.
|
|
/// Returns a value in [0, 1].
|
|
fn prefix_score(label: &str, prefix: &str) -> f32 {
|
|
if prefix.is_empty() {
|
|
return 0.5;
|
|
}
|
|
let lower_label = label.to_lowercase();
|
|
let lower_prefix = prefix.to_lowercase();
|
|
if lower_label.starts_with(&lower_prefix) {
|
|
// Exact prefix match — score by how complete the prefix is
|
|
(lower_prefix.len() as f32 / lower_label.len() as f32).min(1.0)
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
fn keyword_doc(kw: &str) -> Option<String> {
|
|
let doc = match kw {
|
|
"activate" => "Spreading-activation query: `activate TypeName where \"query\"`\nReturns `[TypeName]` from the Engram knowledge graph.",
|
|
"sealed" => "Quantum-sealed block: marks sensitive code for runtime protection.",
|
|
"let" => "Declare an immutable binding: `let name: Type = expr`",
|
|
"fn" => "Define a function: `fn name(params) -> ReturnType { body }`",
|
|
"type" => "Define a struct type: `type Name { field: Type }`",
|
|
"enum" => "Define an enum: `enum Name { Variant1, Variant2(Type) }`",
|
|
"match" => "Pattern match: `match expr { Pattern => result }`",
|
|
"return" => "Return a value from a function.",
|
|
"where" => "Used in `activate T where \"query\"`",
|
|
"if" => "Conditional: `if cond { then } else { else }`",
|
|
"else" => "Else branch of an if expression.",
|
|
"for" => "For loop: `for item in collection { body }`",
|
|
"while" => "While loop: `while cond { body }`",
|
|
"in" => "Used in `for item in collection`",
|
|
"true" | "false" => "Boolean literal",
|
|
"protocol" => "Define a protocol (trait): `protocol Name { fn method(self) -> Ret; }`",
|
|
"impl" => "Implement a protocol: `impl Protocol for Type { fn method(self) -> Ret { ... } }`",
|
|
"import" => "Import from a module: `import { Name } from \"module\"`",
|
|
"from" => "Used in import: `import { Name } from \"module\"`",
|
|
"as" => "Alias in import: `import { Name as Alias } from \"module\"`",
|
|
"with" => "With clause for retry/fallback: `with retry times 3`",
|
|
"retry" => "Retry policy: `retry times N`",
|
|
"times" => "Used in retry: `retry times N`",
|
|
"fallback" => "Fallback value on failure: `fallback { default_expr }`",
|
|
"reason" => "Reason clause: provides context to Engram reasoning engine.",
|
|
"parallel" => "Parallel execution: `parallel { task1; task2 }`",
|
|
"trace" => "Emit a trace event: `trace \"message\"`",
|
|
"requires" => "Dependency declaration: `requires Module`",
|
|
"deploy" => "Deploy declaration: `deploy service to target via method`",
|
|
"to" => "Used in deploy: `deploy X to Y`",
|
|
"via" => "Used in deploy: `deploy X via method`",
|
|
"test" => "Test declaration: `test \"name\" { assertions }`",
|
|
"seed" => "Seed data block: `seed { ... }`",
|
|
"assert" => "Assertion: `assert condition, \"message\"`",
|
|
"target" => "Target annotation: `target { ... }`",
|
|
_ => return None,
|
|
};
|
|
Some(doc.to_string())
|
|
}
|