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
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
include:
- language: javascript-typescript
build-mode: none
- language: actions
build-mode: none

steps:
- name: Checkout
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/instant-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
timeout-minutes: 5
steps:
- name: Trigger Propagation
if: ${{ secrets.FARM_DISPATCH_TOKEN != '' }}
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v3
with:
token: ${{ secrets.FARM_DISPATCH_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/push-email-notify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
name: Email on push
if: ${{ vars.PUSH_EMAIL_ENABLED == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Send push notification email
uses: dawidd6/action-send-mail@6e502825a508b867ab2954ad6343b68787624c01 # pinned
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ permissions:

jobs:
scorecard:
permissions:
contents: read
security-events: write
id-token: write
uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236
2 changes: 2 additions & 0 deletions .github/workflows/status-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
status-gate-core:
name: status-gate-core
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand All @@ -49,6 +50,7 @@ jobs:
status-gate-tests:
name: status-gate-tests (slow · cancellable)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Notice — this is the slow, skippable job
run: |
Expand Down
44 changes: 44 additions & 0 deletions .hypatia-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,47 @@
cicd_rules/banned_language_file:formal/Makefile.coq
cicd_rules/banned_language_file:formal/Makefile.coq.conf
cicd_rules/banned_language_file:formal/Makefile.conf

# ─── Coq proof archaeology — legacy preservation admit (2026-06-24 triage) ─────
#
# formal/Semantics.v carries the LEGACY `preservation` Theorem (declared :8556),
# closed with a DELIBERATE `Admitted.` (:9258). The theorem is provably FALSE —
# formal/Counterexample.v discharges its falsity with 5 Qed lemmas — so the admit
# is owner-mandated (CLAUDE.md 2026-05-27 fence + in-file ARCHAEOLOGY/DO-NOT-EXTEND
# banner). It must never be "fixed"; the principled work is the four-layer redesign
# in formal/PRESERVATION-DESIGN.md (preservation re-derived per layer). File-scoped:
# the whole file is fenced legacy archaeology where any admit re-detection is the
# same accepted non-defect. (Semantics_L1.v / Counterexample_L2_nested.v admits are
# handled by in-file `hypatia: allow` directives, not here.)
code_safety/admitted:formal/Semantics.v

# ─── Stale duplicate linter (lib/linter.rs) ────────────────────────────────────
#
# lib/linter.rs (178 lines) is a stale/legacy sibling of the live
# src/ephapax-tools/src/linter.rs (920 lines). Its two `line.find(..).unwrap_or(0)+1`
# sites compute 1-based diagnostic columns; 0 is a benign "column 1" default, not a
# masked fault. Exempted pending an owner decision on whether to remove the duplicate.
code_safety/unwrap_dangerous_default:lib/linter.rs

# ─── SD009: legacy OCaml files without SPDX headers (src/*.ml) ──────────────────
#
# src/{lexer,parser,ast,typecheck}.ml are legacy OCaml artefacts in a Rust/Coq/Idris
# project (the live type checker is Rust). Adding licence headers is an SPDX edit,
# which estate policy reserves for the owner (no-automated-licence-edits). Flag-only:
# exempted here and surfaced to the owner — these are also candidates for removal.
structural_drift/SD009:src/lexer.ml
structural_drift/SD009:src/parser.ml
structural_drift/SD009:src/ast.ml
structural_drift/SD009:src/typecheck.ml

# ─── SD022: doc references to pre-restructure directories (deferred doc refresh) ─
#
# EXPLAINME/README/ROADMAP.adoc + tests/fuzz/FUZZING_GUIDE.md reference dirs
# (src/core, src/Burble, src/Ephapax, src/parse_fuzzer, src/typecheck_fuzzer) that
# predate the move to the `src/ephapax-*` cargo workspace. This is real (deferred)
# doc-drift, not a false positive; exempted pending an owner doc-refresh
# (prose = CC-BY-SA-4.0, not auto-edited in this security-triage PR).
structural_drift/SD022:EXPLAINME.adoc
structural_drift/SD022:README.adoc
structural_drift/SD022:ROADMAP.adoc
structural_drift/SD022:tests/fuzz/FUZZING_GUIDE.md
2 changes: 2 additions & 0 deletions formal/Counterexample_L2_nested.v
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
(* SPDX-License-Identifier: MPL-2.0 *)
(* Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)
(* hypatia: allow code_safety/admitted -- FALSE POSITIVE: this file has no Coq admit/Admitted/Axiom; all proofs are Qed-closed. The scanner substring-matched prose in the doc comment that NEGATES their presence ("No new [Axiom] or [Admitted] markers"). *)
(* hypatia: allow coq_admit_tactic -- same false positive: substring match on documentation prose, not a Coq tactic. *)

(** * Soundness gap (L2): preservation_l2 fails for nested-TFunEff
substituends — the gap Phase 3b Stage 1's [tfuneff_lambda_free]
Expand Down
3 changes: 3 additions & 0 deletions formal/Semantics_L1.v
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)

(* hypatia: allow code_safety/admitted -- TRACKED v2 proof-debt, not an honest-completion gap. The 3 Admitted lemmas (region_liveness_at_split_l1_gen, step_pop_disjoint_from_type_l1, preservation_l1) and their internal `admit.` tactics are documented in PROOF-NEEDS.md and owner-fenced by CLAUDE.md (directive 2026-05-27): closing or extending them is a banned anti-pattern. region_liveness is provably FALSE as stated; step_pop is open eliminator-fork research; preservation_l1 is the capstone gated on the others. Build truth is the `Print Assumptions` CI gate, not this comment. *)
(* hypatia: allow coq_admit_tactic -- see preceding directive; same tracked admits. *)

(**

*********************************************************************
Expand Down
11 changes: 11 additions & 0 deletions idris2/ffi/zig/tokbuf.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>

const std = @import("std");

const Tok = struct {
Expand Down Expand Up @@ -85,6 +89,13 @@ export fn eph_tokbuf_str_ptr(buf: *TokBuf, idx: i32) [*:0]const u8 {
const i = @as(usize, @intCast(idx));
const t = buf.items[i];
if (t.str_ptr) |p| {
// SAFETY: p is [*]u8 (align 1); cast to the [*:0]const u8 return
// type is alignment- and provenance-preserving and only narrows
// mutability (adds const). The :0 sentinel is upheld by the
// producer copyStr (allocates len+1 and writes a trailing NUL) and
// required by the consumer (Idris FFI binds this to `String`, which
// reads a NUL-terminated C string). The cast asserts, not checks,
// the sentinel; the invariant is guaranteed at the copyStr seam.
return @ptrCast(p);
}
return "";
Expand Down
16 changes: 16 additions & 0 deletions src/ephapax-interp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ impl Interpreter {
/// Loading shared libraries is inherently unsafe. The caller must
/// ensure the library is trustworthy.
pub fn load_ffi_library(&mut self, path: &str) -> Result<(), RuntimeError> {
// SAFETY: Loading a shared library is unsafe because the loaded code
// is unverified; caller trust is documented in this fn's `# Safety`
// section. The handle is stored in `self.ffi_libraries` and kept alive
// until the interpreter is dropped, so no symbol outlives its library.
let lib = unsafe {
libloading::Library::new(path).map_err(|e| {
RuntimeError::Unimplemented(format!("Failed to load FFI library '{}': {}", path, e))
Expand All @@ -373,6 +377,9 @@ impl Interpreter {
) -> Option<libloading::Symbol<'a, unsafe extern "C" fn(i64, i64, i64, i64, i64, i64) -> i64>>
{
for lib in &self.ffi_libraries {
// SAFETY: `lib` is a live, owned library handle borrowed from
// `self.ffi_libraries` for `'a`; the returned Symbol is tied to that
// borrow, so it cannot outlive the library it was resolved from.
if let Ok(sym) = unsafe { lib.get(name.as_bytes()) } {
return Some(sym);
}
Expand Down Expand Up @@ -533,6 +540,11 @@ impl Interpreter {
if let Some(func) = self.find_ffi_symbol(symbol) {
// Pad args to 6 (max C ABI args we support)
let a = |i: usize| *c_args.get(i).unwrap_or(&0);
// SAFETY: `func` was resolved via find_ffi_symbol from a
// loaded library to the 6-arg i64 C-ABI signature it is
// called with here; args are padded to 6 so all params are
// supplied. Soundness ultimately rests on the caller having
// loaded a trustworthy library (see load_ffi_library).
let result = unsafe { func(a(0), a(1), a(2), a(3), a(4), a(5)) };
Ok(Value::I64(result))
} else {
Expand Down Expand Up @@ -1233,6 +1245,10 @@ impl Interpreter {
// until the FFI call completes.
match std::ffi::CString::new(data.as_str()) {
Ok(cstr) => {
// SAFETY: the raw pointer stays valid because `cstr` is
// immediately moved into `cstrings` (kept alive until the
// FFI call completes) on the next line, so `ptr` never
// dangles for the duration of its use.
let ptr = cstr.as_ptr() as i64;
cstrings.push(cstr);
ptr
Expand Down
3 changes: 3 additions & 0 deletions src/ephapax-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell
// hypatia: allow code_safety/unwrap_dangerous_default -- the two integer-literal `parse().unwrap_or(0)` sites (see inline notes) only fall back on i64 overflow in a fn returning `Literal` (no Result); 0 is a deliberate lossy fallback pending the parse_literal Result refactor.

//! Ephapax Parser
//!
Expand Down Expand Up @@ -953,6 +954,7 @@ fn parse_literal_value_core(pair: pest::iterators::Pair<Rule>) -> Literal {
Rule::boolean => Literal::Bool(pair.as_str() == "true"),
Rule::float => Literal::F64(pair.as_str().parse().unwrap_or(0.0)),
Rule::integer => {
// hypatia: allow code_safety/unwrap_dangerous_default -- grammar `integer = ASCII_DIGIT+` (ephapax.pest) means parse() can only fail on i64 overflow; this fn returns `Literal` (no Result), so 0 is a deliberate lossy fallback. TODO: propagate overflow as ParseError (ripples to callers at lib.rs:896/978).
let n: i64 = pair.as_str().parse().unwrap_or(0);
if n > i32::MAX as i64 || n < i32::MIN as i64 {
Literal::I64(n)
Expand Down Expand Up @@ -1993,6 +1995,7 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Expr, ParseError>
Literal::F64(n)
}
Rule::integer => {
// hypatia: allow code_safety/unwrap_dangerous_default -- as above: parse() only fails on i64 overflow and there is no Result to propagate here, so 0 is a deliberate lossy fallback pending the parse_literal Result refactor.
let n: i64 = inner.as_str().parse().unwrap_or(0);
if n > i32::MAX as i64 || n < i32::MIN as i64 {
Literal::I64(n)
Expand Down
15 changes: 9 additions & 6 deletions src/ephapax-query/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ impl QueryDb {
/// text.
pub fn parsed(&mut self, id: &str) -> ParsedResult {
let dep = self.input_changed_at(id);
if let Some(m) = self.parsed.get(id) {
let rev = self.revision;
if let Some(m) = self.parsed.get_mut(id) {
if dep <= m.verified_at {
let value = m.value.clone();
self.parsed.get_mut(id).unwrap().verified_at = self.revision;
m.verified_at = rev;
return value;
}
}
Expand All @@ -161,10 +162,11 @@ impl QueryDb {
pub fn typed(&mut self, id: &str) -> TypedResult {
let parsed = self.parsed(id);
let dep = self.parsed.get(id).map(|m| m.changed_at).unwrap_or_default();
if let Some(m) = self.typed.get(id) {
let rev = self.revision;
if let Some(m) = self.typed.get_mut(id) {
if dep <= m.verified_at {
let value = m.value.clone();
self.typed.get_mut(id).unwrap().verified_at = self.revision;
m.verified_at = rev;
return value;
}
}
Expand Down Expand Up @@ -195,10 +197,11 @@ impl QueryDb {
let dep_parsed = self.parsed.get(id).map(|m| m.changed_at).unwrap_or_default();
let dep_typed = self.typed.get(id).map(|m| m.changed_at).unwrap_or_default();
let dep = dep_parsed.max(dep_typed);
if let Some(m) = self.wasm.get(id) {
let rev = self.revision;
if let Some(m) = self.wasm.get_mut(id) {
if dep <= m.verified_at {
let value = m.value.clone();
self.wasm.get_mut(id).unwrap().verified_at = self.revision;
m.verified_at = rev;
return value;
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/ephapax-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ pub unsafe extern "C" fn __ephapax_string_eq(h1: StringHandle, h2: StringHandle)
return 1; // Both empty
}

// SAFETY: Both handles are valid string handles (function contract above), so
// `ptr1`/`ptr2` and `len1`/`len2` are matched ptr+len pairs as written by
// `__ephapax_string_new`, each pointing at `len` valid bytes. The `len1 == 0`
// early-return above guarantees `len > 0` here, and the `len1 != len2` guard
// makes the two slices equal length. The slices are read-only and never escape
// this function.
let slice1 = core::slice::from_raw_parts(ptr1 as *const u8, len1 as usize);
let slice2 = core::slice::from_raw_parts(ptr2 as *const u8, len2 as usize);

Expand Down
6 changes: 6 additions & 0 deletions src/ephapax-tools/src/linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
//! require a parsed AST, making it suitable for use in the standalone LSP,
//! BoJ lsp-mcp cartridge, and CI pipelines.

// hypatia: allow code_safety/unwrap_dangerous_default -- `line.find(&pat).unwrap_or(0) + 1`
// computes a 1-based column for a diagnostic caret. The pattern is extracted from the same
// line just above, so find() effectively always succeeds; the 0 fallback (-> column 1) is a
// benign "point at line start" default, never a masked fault. Tooling crate (linter); a wrong
// caret column cannot corrupt analysis output.

use std::collections::{HashMap, HashSet};

/// Lint severity levels.
Expand Down
2 changes: 2 additions & 0 deletions src/ephapax-vram-cache/benches/cache_bench.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
// SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell
//
// hypatia: allow code_safety/unwrap_without_check -- Criterion benchmark file (harness = false); unwrap on cache setup and inside b.iter loops is idiomatic and intentional (a failed setup should abort the bench loudly). Not library code.

use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use ephapax_vram_cache::VramCache;
Expand Down
2 changes: 2 additions & 0 deletions src/ephapax-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: MPL-2.0
// Owner: Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//! SPDX-FileCopyrightText: 2025 Jonathan D.A. Jewell
// hypatia: allow code_safety/unwrap_dangerous_default -- the single `data_info...unwrap_or(0)` site (see inline note) uses 0 as a benign sentinel (None => non-data scrutinee), never a masked fault.

//! Ephapax WASM Code Generator (Dyadic: Affine + Linear modes)
//!
Expand Down Expand Up @@ -2402,6 +2403,7 @@ impl Codegen {
let payload_local = self.locals.temp();
let tag_local = self.locals.temp();

// hypatia: allow code_safety/unwrap_dangerous_default -- 0 is a benign sentinel, not a masked fault: `data_info` is None for non-data scrutinees (Bool/Pair/Tuple/Unit/Int); a real data type always has total = constructors.len() >= 1, so 0 unambiguously means "no multi-ctor tag walk" and is routed correctly by the `total >= 2` check below.
let total = data_info.as_ref().map(|(_, t)| *t).unwrap_or(0);
if total >= 2 {
let cur_local = self.locals.temp();
Expand Down
Loading