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
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 @@ -57,6 +57,7 @@ sha2 = "0.10"
hmac = "0.12"
hex = "0.4"
subtle = "2"
semver = "1"

[dev-dependencies]
wiremock = "0.6"
Expand Down
52 changes: 52 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1830,6 +1830,58 @@ Declaring a private function: `_helper-name params:type > return-type; body`

---

## Package Registry

ilo has a lightweight GitHub-based package registry. There is no central server — GitHub is the substrate.

### Installing packages

```
ilo add <owner>/<repo> -- fetch latest default branch
ilo add <owner>/<repo>@<ref> -- fetch a specific branch, tag, or SHA prefix
ilo update -- re-fetch all installed packages
ilo update <owner>/<repo> -- re-fetch one package
```

`ilo add` performs a shallow `git clone` into `~/.ilo/pkgs/<owner>/<repo>/` and writes a line to `ilo.lock` in the current directory.

### Using installed packages

After `ilo add myorg/helpers`, import the package's `index.ilo` with:

```
use "myorg/helpers" -- imports ~/.ilo/pkgs/myorg/helpers/index.ilo
use "myorg/helpers" [foo bar] -- selective import
use "myorg/helpers/utils.ilo" -- import a specific file from the package
```

A `use` path whose first component contains no `.` is treated as a package reference, not a local file path. To import a local file in a sibling directory, use an explicit leading `./`:

```
use "./sibling.ilo" -- always local
use "myorg/helpers" -- always a package
```

### Lockfile (`ilo.lock`)

`ilo add` writes/updates `ilo.lock` in the current working directory. Commit this file to source control.

```
# ilo.lock — generated by `ilo add`; commit to source control
myorg/helpers <sha40> https://github.com/myorg/helpers
```

Format: tab-separated columns `slug`, `sha`, `url`. Lines starting with `#` are comments.

### Non-goals (v1)

- Centralised registry hosting (GitHub is the substrate)
- Semantic versioning enforcement
- Private registry / auth
- Transitive dependency resolution

---

## Error Handling

`R ok err` return type. Call then match:
Expand Down
21 changes: 21 additions & 0 deletions examples/pkg-registry.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- pkg-registry.ilo
-- Demonstrates `use "owner/repo"` package import syntax.
--
-- Before running:
-- ilo add ilo-lang/ilo-std
--
-- Then:
-- ilo run examples/pkg-registry.ilo
--
-- This file won't run standalone (the package won't exist on your machine
-- unless you `ilo add` it), but it serves as a usage reference.
--
-- use "ilo-lang/ilo-std" -- import index.ilo from cached package
-- use "ilo-lang/ilo-std" [greet] -- selective import
--
-- main>_;
-- prnt greet "agent"

-- Minimal standalone demo that works without a network package:
main>_;
prnt "run `ilo add owner/repo` to install a package, then `use \"owner/repo\"` to import it"
34 changes: 26 additions & 8 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ pub enum Cmd {
/// Print version.
Version,

/// Fetch a GitHub-hosted ilo package into the local cache (~/.ilo/pkgs/).
Add(AddArgs),

/// Re-fetch a cached package to its latest commit on the default branch.
Update(UpdateArgs),
/// Trace program execution, emitting one JSON line per statement.
Trace(TraceArgs),
}
Expand Down Expand Up @@ -449,8 +454,25 @@ pub enum SkillCmd {
Show { name: String },
}

// ── Trace ──────────────────────────────────────────────────────────────────────
// ── Add ────────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct AddArgs {
/// Package to fetch, in `<owner>/<repo>` or `<owner>/<repo>@<ref>` form.
/// Example: `ilo add myorg/helpers` or `ilo add myorg/helpers@v1.2`.
pub package: String,
}

// ── Update ─────────────────────────────────────────────────────────────────────

#[derive(Args, Debug)]
pub struct UpdateArgs {
/// Package to update, in `<owner>/<repo>` form.
/// Omit to update all packages recorded in `ilo.lock`.
pub package: Option<String>,
}

// ── Trace ──────────────────────────────────────────────────────────────────────
/// Granularity of trace events.
#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TraceDepth {
Expand All @@ -460,28 +482,24 @@ pub enum TraceDepth {
/// Emit one event per sub-expression in addition to per-statement events.
Expr,
}

#[derive(Args, Debug)]
#[derive(Args, Debug, Clone)]
pub struct TraceArgs {
/// Source file to trace.
#[arg(value_name = "FILE")]
pub source: String,

/// Entry function name (defaults to first function).
#[arg(long = "func", value_name = "NAME")]
pub func: Option<String>,

/// Trace granularity: `statement` (default) or `expr` (per sub-expression).
#[arg(long = "depth", value_enum, default_value = "statement")]
pub depth: TraceDepth,

/// Only emit events that touch this variable name (may be repeated).
#[arg(long = "watch", value_name = "NAME", action = clap::ArgAction::Append)]
pub watch: Vec<String>,

/// Call arguments passed to the entry function.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub rest: Vec<String>,
}

// ── OutputMode resolution ──────────────────────────────────────────────────────

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod rng;
pub mod interpreter;
pub mod lexer;
pub mod parser;
pub mod pkg;
pub mod runtime_guard;
pub mod tools;
pub mod verify;
Expand Down
152 changes: 152 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2280,6 +2280,41 @@ impl BuildTarget {
}
}

/// Apply the `only [name1 name2]` filter from a `use` statement.
/// Pushes `ILO-P019` diagnostics for names not found.
fn apply_only_filter(
decls: Vec<ast::Decl>,
only: &Option<Vec<String>>,
path: &str,
span: ast::Span,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<ast::Decl> {
let Some(names) = only else {
return decls;
};
for name in names {
let found = decls.iter().any(|d| decl_name(d) == Some(name.as_str()));
if !found {
diagnostics.push(
Diagnostic::error(format!(
"use \"{}\": name '{}' not found in imported file",
path, name
))
.with_code("ILO-P019")
.with_span(span, "imported here"),
);
}
}
decls
.into_iter()
.filter(|d| {
decl_name(d)
.map(|n| names.iter().any(|s| s == n))
.unwrap_or(false)
})
.collect()
}

/// Resolve all `Decl::Use` nodes in `decls` recursively, returning a flat
/// merged list with imported declarations prepended and `Use` nodes stripped.
///
Expand Down Expand Up @@ -2323,7 +2358,122 @@ fn resolve_imports(
path
};

// ── Package registry resolution ───────────────────────────────────
// `use "owner/repo"` (first component has no `.`) resolves through
// the local package cache at ~/.ilo/pkgs/<owner>/<repo>/index.ilo.
if ilo::pkg::is_pkg_path(&path) {
let resolved = ilo::pkg::resolve_pkg_path(&path);
match resolved {
Err(msg) => {
diagnostics.push(
Diagnostic::error(format!("use \"{path}\": {msg}"))
.with_code("ILO-P017")
.with_span(span, "imported here"),
);
continue;
}
Ok(pkg_file) => {
// Synthesise a Use decl for the resolved file path and
// re-use the same local-file resolution path below by
// substituting the resolved path into a local Use node.
let abs = pkg_file.to_string_lossy().into_owned();
let synthetic = ast::Decl::Use {
path: abs,
only: only.clone(),
alias: alias.clone(),
predicate: None,
alt_path: None,
span,
};
let mut sub = resolve_imports(
vec![synthetic],
None, // abs path, base_dir not needed
visited,
diagnostics,
build_target,
);
result.append(&mut sub);
continue;
}
}
}

// ── Local file resolution ─────────────────────────────────────────
let Some(dir) = base_dir else {
// Package paths are handled above; inline code cannot use local files.
// Absolute paths (synthesised by package resolution) are also handled above.
if path.starts_with('/') {
// Absolute path synthesised by package resolution — resolve directly.
let canonical = match std::path::PathBuf::from(&path).canonicalize() {
Ok(c) => c,
Err(_) => {
diagnostics.push(
Diagnostic::error(format!("use \"{}\": file not found", path))
.with_code("ILO-P017")
.with_span(span, "imported here"),
);
continue;
}
};
// Proceed with the canonical path as if base_dir were its parent.
let imported_dir = canonical.parent().map(|p| p.to_path_buf());
let source = match std::fs::read_to_string(&canonical) {
Ok(s) => s,
Err(e) => {
diagnostics.push(
Diagnostic::error(format!("use \"{}\": {}", path, e))
.with_code("ILO-P017")
.with_span(span, "imported here"),
);
continue;
}
};
if visited.contains(&canonical) {
diagnostics.push(
Diagnostic::error(format!("use \"{}\": circular import", path))
.with_code("ILO-P018")
.with_span(span, "imported here"),
);
continue;
}
let tokens = match lexer::lex(&source) {
Ok(t) => t,
Err(e) => {
diagnostics.push(Diagnostic::from(&e));
continue;
}
};
let token_spans: Vec<(lexer::Token, ast::Span)> = tokens
.into_iter()
.map(|(t, r)| {
(
t,
ast::Span {
start: r.start,
end: r.end,
},
)
})
.collect();
let (mut imported_prog, parse_errors) = parser::parse(token_spans);
ast::resolve_aliases(&mut imported_prog);
ast::desugar_dot_var_index(&mut imported_prog);
for e in &parse_errors {
diagnostics.push(Diagnostic::from(e));
}
visited.insert(canonical.clone());
let imported_decls = resolve_imports(
imported_prog.declarations,
imported_dir.as_deref(),
visited,
diagnostics,
);
visited.remove(&canonical);
let filtered =
apply_only_filter(imported_decls, &only, &path, span, diagnostics);
result.extend(filtered);
continue;
}
diagnostics.push(
Diagnostic::error(
"`use` requires a file path context — not supported in inline code",
Expand Down Expand Up @@ -2924,6 +3074,8 @@ fn dispatch_cli(cli: cli::Cli, bare_has_bin: bool) -> i32 {
Some(cli::Cmd::Test(t)) => cli::test_runner::run(t),
Some(cli::Cmd::Trace(t)) => cli::trace::run(t),
Some(cli::Cmd::Version) => version_cmd(cli.global.explicit_json()),
Some(cli::Cmd::Add(a)) => std::process::exit(ilo::pkg::cmd_add(&a.package)),
Some(cli::Cmd::Update(u)) => std::process::exit(ilo::pkg::cmd_update(u.package.as_deref())),
Some(cli::Cmd::Run(r)) => {
let mode = cli.global.output_mode();
let explicit_json = cli.global.explicit_json();
Expand Down
Loading
Loading