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
42 changes: 42 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: MPL-2.0
# .editorconfig — RSR standard editor configuration
# https://editorconfig.org

root = true

Expand All @@ -11,3 +12,44 @@ end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

# Prose formats rely on trailing double-space for hard line breaks.
[*.md]
trim_trailing_whitespace = false

[*.adoc]
trim_trailing_whitespace = false

# Per-language indent widths.
[*.rs]
indent_size = 4

[*.zig]
indent_size = 4

[*.{ada,adb,ads}]
indent_size = 3

[*.{ex,exs}]
indent_size = 2

[*.hs]
indent_size = 2

[*.{res,resi}]
indent_size = 2

[*.ncl]
indent_size = 2

[*.rkt]
indent_size = 2

[*.scm]
indent_size = 2

[*.nix]
indent_size = 2

[{Justfile,justfile}]
indent_style = space
indent_size = 4
13 changes: 11 additions & 2 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
# Rust CI — thin wrapper calling the shared estate reusable in
# hyperpolymath/standards. Configure once, propagate everywhere.
# See: docs/CI-REUSABLE-WORKFLOWS.adoc in standards.
#
# DO NOT reintroduce a job-level `if: hashFiles('Cargo.toml') != ''` guard here.
# Job-level `if:` is evaluated server-side before any checkout, so hashFiles()
# has no workspace to hash. This workflow carried that guard on both local jobs
# and, as a result, failed 60/60 runs at 0s with zero jobs and no logs — a
# startup failure, not a test failure. GitHub reported the workflow's name as
# `.github/workflows/rust-ci.yml` (the path-fallback used when a file has never
# been parsed) rather than the declared `Rust CI`, which is how it was found.
# Net effect: this repo's Rust build/test gate never ran between 2026-06-27 and
# 2026-07-27. If a Cargo.toml presence check is ever needed, follow the estate
# reusable and gate on a `detect` job output, not on hashFiles() at job level.
name: Rust CI

on:
Expand All @@ -24,7 +35,6 @@ jobs:
name: Cargo build + test (ephapax-cli, --no-default-features)
runs-on: ubuntu-latest
timeout-minutes: 20
if: hashFiles('Cargo.toml') != ''

# Proves ephapax can be built and tested with zero git dep on the
# sibling `hyperpolymath/typed-wasm` repo. Enforces the estate
Expand Down Expand Up @@ -64,7 +74,6 @@ jobs:
name: wasm-tools validate (emitted modules)
runs-on: ubuntu-latest
timeout-minutes: 20
if: hashFiles('Cargo.toml') != ''

# Structurally validates every module ephapax emits for the fixture
# corpus (`just validate-wasm`), catching codegen that produces an
Expand Down
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,36 @@ dist/
*.cmi
*.cmo
*.cmx

# Additional build/tooling artefacts (salvaged from the 2026-07-26 sweep;
# `.claude/` and `.editorconfig` were dropped from that set — both are TRACKED
# in this repo and ignoring them would hide real changes from `git status`).
*.ali
*.chpl.tmp.*
*.db
*.db-journal
*.db-shm
*.db-wal
*.ez
*.jl.cov
*.jl.mem
*.py[cod]
.venv/
/.elixir_ls/
/.stack-work/
/Manifest.toml
/_build/
/bin/
/build/
/cover/
/dist-newstyle/
/dist/
/doc/
/exports/*.json
/exports/*.lgt
/obj/
/out/
__pycache__/
composer/*.beam
composer/build/
erl_crash.dump
67 changes: 49 additions & 18 deletions ephapax-linear/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,19 +422,29 @@ fn p2p_contraction_always_rejected_both_disciplines() {

let mut linear = LinearChecker::new();
let result = linear.check(&expr);
assert!(result.is_err(), "P2P linear[{i}]: double use must be rejected");
assert!(
result.is_err(),
"P2P linear[{i}]: double use must be rejected"
);
let violations = result.unwrap_err();
assert!(
violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
violations
.iter()
.any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
"P2P linear[{i}]: must report Contraction violation"
);

let mut affine = AffineChecker::new();
let result = affine.check(&expr);
assert!(result.is_err(), "P2P affine[{i}]: double use must be rejected");
assert!(
result.is_err(),
"P2P affine[{i}]: double use must be rejected"
);
let violations = result.unwrap_err();
assert!(
violations.iter().any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
violations
.iter()
.any(|v| matches!(v, DisciplineViolation::Contraction { .. })),
"P2P affine[{i}]: must report Contraction violation"
);
}
Expand All @@ -459,10 +469,15 @@ fn p2p_weakening_linear_forbidden_affine_allowed() {
// Linear must reject
let mut linear = LinearChecker::new();
let result = linear.check(&expr);
assert!(result.is_err(), "P2P linear[{i}]: weakening must be rejected");
assert!(
result.is_err(),
"P2P linear[{i}]: weakening must be rejected"
);
let violations = result.unwrap_err();
assert!(
violations.iter().any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })),
violations
.iter()
.any(|v| matches!(v, DisciplineViolation::WeakeningForbidden { .. })),
"P2P linear[{i}]: must report WeakeningForbidden"
);

Expand Down Expand Up @@ -790,7 +805,10 @@ fn aspect_checker_stateless_between_instances() {
});

let mut checker1 = LinearChecker::new();
assert!(checker1.check(&bad_expr).is_err(), "Aspect stateless: bad expr must fail");
assert!(
checker1.check(&bad_expr).is_err(),
"Aspect stateless: bad expr must fail"
);

// Second call with a DIFFERENT checker instance on a good expression
let good_expr = e(ExprKind::LetLin {
Expand Down Expand Up @@ -835,10 +853,7 @@ fn aspect_violations_non_empty_on_failure() {
body: Box::new(unit()),
}),
),
(
"drop",
e(ExprKind::Drop(Box::new(i32_lit(3)))),
),
("drop", e(ExprKind::Drop(Box::new(i32_lit(3))))),
];

for (label, expr) in &bad_cases {
Expand Down Expand Up @@ -1037,14 +1052,30 @@ fn aspect_violation_error_warning_mutually_exclusive() {
use crate::DisciplineViolation;

let all_violations: Vec<DisciplineViolation> = vec![
DisciplineViolation::Contraction { name: "x".to_string() },
DisciplineViolation::NotInScope { name: "y".to_string() },
DisciplineViolation::WeakeningForbidden { name: "z".to_string() },
DisciplineViolation::Contraction {
name: "x".to_string(),
},
DisciplineViolation::NotInScope {
name: "y".to_string(),
},
DisciplineViolation::WeakeningForbidden {
name: "z".to_string(),
},
DisciplineViolation::DropForbidden,
DisciplineViolation::BranchDisagreement { name: "b".to_string() },
DisciplineViolation::RegionLeakLinear { region: "r".to_string(), name: "s".to_string() },
DisciplineViolation::LetForLinearType { name: "t".to_string() },
DisciplineViolation::ImplicitDropWarning { region: "r".to_string(), name: "u".to_string() },
DisciplineViolation::BranchDisagreement {
name: "b".to_string(),
},
DisciplineViolation::RegionLeakLinear {
region: "r".to_string(),
name: "s".to_string(),
},
DisciplineViolation::LetForLinearType {
name: "t".to_string(),
},
DisciplineViolation::ImplicitDropWarning {
region: "r".to_string(),
name: "u".to_string(),
},
];

for violation in &all_violations {
Expand Down
31 changes: 15 additions & 16 deletions src/ephapax-cli/src/import_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,7 @@ fn first_module_declaration(source: &str) -> Option<String> {
let rest = rest.trim();
// Take everything up to a whitespace, comma, or comment marker.
let end = rest
.find(|c: char| {
!(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/')
})
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/'))
.unwrap_or(rest.len());
let name = &rest[..end];
if !name.is_empty() {
Expand Down Expand Up @@ -208,15 +206,14 @@ fn visit(
message: e.to_string(),
})?;

let surface =
parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse {
path: file_path.clone(),
message: errs
.iter()
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.join("; "),
})?;
let surface = parse_surface_module(&source, logical).map_err(|errs| ResolveError::Parse {
path: file_path.clone(),
message: errs
.iter()
.map(|e| format!("{}", e))
.collect::<Vec<_>>()
.join("; "),
})?;

// Recurse into imports BEFORE inserting this module so that the
// post-order visit places dependencies before this module.
Expand All @@ -226,7 +223,9 @@ fn visit(
.map(|i| normalise_path(i.module.as_str()))
.collect();
for dep in &deps {
visit(dep, None, base_dir, mod_index, loaded, order, visiting, stack)?;
visit(
dep, None, base_dir, mod_index, loaded, order, visiting, stack,
)?;
}

loaded.insert(
Expand All @@ -253,9 +252,9 @@ fn root_module_path_from_source(root_path: &Path) -> Result<String, ResolveError
let line = line.trim_start();
if let Some(rest) = line.strip_prefix("module") {
let rest = rest.trim();
if let Some(end) = rest.find(|c: char| {
!(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/')
}) {
if let Some(end) = rest
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '/'))
{
return Ok(normalise_path(&rest[..end]));
} else if !rest.is_empty() {
return Ok(normalise_path(rest));
Expand Down
34 changes: 22 additions & 12 deletions src/ephapax-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,16 @@ fn compile_file(
if verbose {
println!("{} Type check passed", "✓".green());
}
return finish_compile(module, &filename, path, output, opt_level, debug,
verify_ownership, verbose);
return finish_compile(
module,
&filename,
path,
output,
opt_level,
debug,
verify_ownership,
verbose,
);
}
Err(e) => return Err(e.to_string()),
};
Expand Down Expand Up @@ -548,8 +556,16 @@ fn compile_file(
println!("{} Type check passed", "✓".green());
}

finish_compile(module, &filename, path, output, opt_level, debug,
verify_ownership, verbose)
finish_compile(
module,
&filename,
path,
output,
opt_level,
debug,
verify_ownership,
verbose,
)
}

/// Codegen + optional ownership verification + output write. Shared by
Expand All @@ -564,7 +580,6 @@ fn finish_compile(
verify_ownership: bool,
verbose: bool,
) -> Result<(), String> {

// Compile to WASM (with or without debug info)
let wasm_bytes = if debug {
ephapax_wasm::compile_module(&module).map_err(|e| format!("Codegen error: {}", e))?
Expand Down Expand Up @@ -672,16 +687,11 @@ fn report_ownership_verification(wasm_bytes: &[u8], verbose: bool) -> Result<(),
match verify_from_module(wasm_bytes) {
Ok(()) => {
if verbose {
println!(
"{} typed-wasm L7+L10 verification: clean",
"✓".green()
);
println!("{} typed-wasm L7+L10 verification: clean", "✓".green());
}
Ok(())
}
Err(VerifyError::Parse(e)) => {
Err(format!("verifier could not parse emitted wasm: {}", e))
}
Err(VerifyError::Parse(e)) => Err(format!("verifier could not parse emitted wasm: {}", e)),
Err(VerifyError::Ownership(errs)) => {
eprintln!(
"{} typed-wasm verification failed: {} violation(s)",
Expand Down
Loading
Loading