Skip to content

Commit ce5e755

Browse files
committed
merge: resolve conflicts in bet-wasm
2 parents 9ce6368 + f925b52 commit ce5e755

36 files changed

Lines changed: 797 additions & 480 deletions

File tree

.github/workflows/scorecard-enforcer.yml

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,24 @@ concurrency:
1717
group: ${{ github.workflow }}-${{ github.ref }}
1818
cancel-in-progress: true
1919

20-
permissions: read-all
20+
permissions:
21+
contents: read
2122

2223
jobs:
24+
# Publish job. The OSSF attestation flow requires that a job invoking
25+
# ossf/scorecard-action with publish_results: true contain ONLY `uses:`
26+
# steps (no `run:`). A `run:` step in this job makes the OSSF publish
27+
# endpoint reject the upload with HTTP 400 ("scorecard job must only have
28+
# steps with `uses`"), failing the whole workflow. Score enforcement is
29+
# therefore split into the separate unprivileged `score-gate` job below.
2330
scorecard:
2431
runs-on: ubuntu-latest
25-
timeout-minutes: 30
32+
timeout-minutes: 15
2633
permissions:
2734
security-events: write
2835
id-token: write # For OIDC
2936
steps:
30-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
37+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3138
with:
3239
persist-credentials: false
3340

@@ -39,14 +46,39 @@ jobs:
3946
publish_results: true
4047

4148
- name: Upload SARIF
42-
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v3
49+
uses: github/codeql-action/upload-sarif@c6f931105cb2c34c8f901cc885ba1e2e259cf745 # v4
4350
with:
4451
sarif_file: results.sarif
4552

53+
# Unprivileged enforcement gate. Re-derives the score read-only (no
54+
# publish, no id-token), so it may legally contain a `run:` step.
55+
# NOTE: uses JSON output deliberately. The SARIF format does NOT carry the
56+
# aggregate score at .runs[0].tool.driver.properties.score (it is absent
57+
# there, so a SARIF-based gate reads the `// 0` fallback and ALWAYS fails
58+
# the < MIN_SCORE check); the JSON format exposes the aggregate at the
59+
# top-level `.score`. publish_results is false here and does not affect the
60+
# computed score, so enforcement behaviour is unchanged.
61+
score-gate:
62+
runs-on: ubuntu-latest
63+
timeout-minutes: 15
64+
permissions:
65+
contents: read
66+
steps:
67+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
68+
with:
69+
persist-credentials: false
70+
71+
- name: Run Scorecard (analysis only)
72+
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
73+
with:
74+
results_file: results.json
75+
results_format: json
76+
publish_results: false
77+
4678
- name: Check minimum score
4779
run: |
48-
# Parse score from results
49-
SCORE=$(jq -r '.runs[0].tool.driver.properties.score // 0' results.sarif 2>/dev/null || echo "0")
80+
# JSON output carries the aggregate score at the top-level `.score`.
81+
SCORE=$(jq -r '.score // 0' results.json 2>/dev/null || echo "0")
5082
5183
echo "OpenSSF Scorecard Score: $SCORE"
5284
@@ -61,9 +93,9 @@ jobs:
6193
# Check specific high-priority items
6294
check-critical:
6395
runs-on: ubuntu-latest
64-
timeout-minutes: 30
96+
timeout-minutes: 15
6597
steps:
66-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v4
98+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6799

68100
- name: Check SECURITY.md exists
69101
run: |

Justfile

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// SPDX-License-Identifier: MPL-2.0
2-
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
33
# betlang - Development Tasks
44
# AUTHORITY: AUTHORITY_STACK.mustfile-nickel.scm
55
# All operations MUST be invoked via `just <recipe>`.
@@ -84,6 +84,34 @@ test-tooling:
8484
clean-tooling:
8585
cargo clean
8686

87+
# --- Rust toolchain recipes (the working compiler/interpreter pipeline) ------
88+
# These drive the real multi-crate Rust implementation under compiler/, runtime/
89+
# and tools/. (The Racket recipes above target the historical core/*.rkt tree.)
90+
91+
# Build the entire Rust workspace (all crates)
92+
build-rust:
93+
cargo build --workspace
94+
95+
# Test the entire Rust workspace
96+
test-rust:
97+
cargo test --workspace
98+
99+
# Type-check a betlang source file with the Rust checker
100+
check FILE:
101+
cargo run -q -p bet -- check {{FILE}}
102+
103+
# Run a betlang source file with the Rust interpreter
104+
run FILE:
105+
cargo run -q -p bet -- run {{FILE}}
106+
107+
# Compile a betlang source file to a backend (TARGET = js | llvm | beam)
108+
compile FILE TARGET="js":
109+
cargo run -q -p bet -- compile {{FILE}} --target {{TARGET}}
110+
111+
# Start the Rust REPL
112+
repl-rust:
113+
cargo run -q -p bet -- repl
114+
87115
# ============================================================================
88116
# PROOFS (formal verification — see docs/AFFINESCRIPT-ALIGNMENT.adoc)
89117
# ============================================================================

bindings/chapel/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ libc = "0.2"
1818
# Core
1919
rand.workspace = true
2020
rand_distr.workspace = true
21+
rand_pcg.workspace = true # seedable PCG generator for `bet_seed`
2122

2223
[build-dependencies]
2324
cbindgen = "0.28"

bindings/chapel/build.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ fn main() {
1010
.with_language(cbindgen::Language::C)
1111
.with_include_guard("BET_CHAPEL_H")
1212
.with_documentation(true)
13+
// Preserve the SPDX/copyright header on the generated file. cbindgen
14+
// overwrites include/betlang.h wholesale on every build; without this
15+
// the licence header is silently stripped each time (an automated
16+
// licence edit, which estate policy forbids — keep it manual & stable).
17+
.with_header(
18+
"// SPDX-License-Identifier: MPL-2.0\n\
19+
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>\n\
20+
// GENERATED by cbindgen (build.rs) — do not edit by hand.",
21+
)
1322
.generate()
1423
.expect("Unable to generate bindings")
1524
.write_to_file("include/betlang.h");

bindings/chapel/include/betlang.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// SPDX-License-Identifier: MPL-2.0
22
// Copyright (c) Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
// GENERATED by cbindgen (build.rs) — do not edit by hand.
4+
35
#ifndef BET_CHAPEL_H
46
#define BET_CHAPEL_H
57

@@ -143,4 +145,4 @@ void bet_seed(uint64_t seed);
143145
*/
144146
const char *bet_version(void);
145147

146-
#endif /* BET_CHAPEL_H */
148+
#endif /* BET_CHAPEL_H */

compiler/bet-check/src/lib.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,10 @@ fn check_bet(bet: &BetExpr, env: &mut CheckEnv, span: Span) -> CompileResult<Typ
837837

838838
env.unify(&t0, &t1, Some(span))?;
839839
env.unify(&t0, &t2, Some(span))?;
840-
Ok(env.resolve(&t0))
840+
// A `bet` introduces probabilistic choice: it has type `Dist T`, not `T`.
841+
// (Matches the mechanised rule `tBet : … → HasType … (Ty.dist T)` in
842+
// proofs/BetLang.lean; eliminate with `sample : Dist 'a -> 'a`.)
843+
Ok(Type::Dist(Box::new(env.resolve(&t0))))
841844
}
842845

843846
/// Check a weighted bet: branches unify, weights must be numeric.
@@ -863,7 +866,8 @@ fn check_weighted_bet(
863866
for i in 1..branch_types.len() {
864867
env.unify(&branch_types[0], &branch_types[i], Some(span))?;
865868
}
866-
Ok(env.resolve(&branch_types[0]))
869+
// Weighted bet, like uniform bet, is a distribution: `Dist T`.
870+
Ok(Type::Dist(Box::new(env.resolve(&branch_types[0]))))
867871
}
868872

869873
/// Check a conditional bet: condition is Bool/Ternary, all branches unify.
@@ -892,7 +896,9 @@ fn check_conditional_bet(
892896
env.unify(&true_ty, &f0, Some(span))?;
893897
env.unify(&true_ty, &f1, Some(span))?;
894898
env.unify(&true_ty, &f2, Some(span))?;
895-
Ok(env.resolve(&true_ty))
899+
// Conditional bet also yields a distribution over `T`: point-mass on the
900+
// `if_true` value when the condition holds, else the ternary distribution.
901+
Ok(Type::Dist(Box::new(env.resolve(&true_ty))))
896902
}
897903

898904
// ============================================
@@ -1265,7 +1271,11 @@ mod tests {
12651271
],
12661272
};
12671273
let result = check_expr(&dummy(Expr::Bet(bet)), &mut env);
1268-
assert_eq!(result.expect("TODO: handle error"), Type::Int);
1274+
// `bet` introduces probabilistic choice, so its type is `Dist Int`.
1275+
assert_eq!(
1276+
result.expect("TODO: handle error"),
1277+
Type::Dist(Box::new(Type::Int))
1278+
);
12691279
}
12701280

12711281
#[test]
@@ -1699,9 +1709,11 @@ mod tests {
16991709
);
17001710
}
17011711

1702-
/// The core primitive bridges to Echo by composition at the type level:
1703-
/// `echo(bet a b c) : Echo T`. (Runtime branch-tag retention — `bet_echo` —
1704-
/// remains deferred; the *type* story needs no new primitive.)
1712+
/// The core primitive bridges to Echo by composition at the type level.
1713+
/// Since `bet a b c : Dist T`, the composite is `echo(bet a b c) : Echo (Dist T)`
1714+
/// — the retained-loss residue over the *distribution*. (Runtime branch-tag
1715+
/// retention — `bet_echo` — remains deferred; the *type* story needs no new
1716+
/// primitive.)
17051717
#[test]
17061718
fn test_bet_echo_bridge_by_composition() {
17071719
let mut env = CheckEnv::new();
@@ -1714,8 +1726,8 @@ mod tests {
17141726
};
17151727
let e = call("echo", vec![dummy(Expr::Bet(bet))]);
17161728
assert_eq!(
1717-
check_expr(&e, &mut env).expect("echo (bet 1 2 3) : Echo Int"),
1718-
Type::Echo(Box::new(Type::Int))
1729+
check_expr(&e, &mut env).expect("echo (bet 1 2 3) : Echo (Dist Int)"),
1730+
Type::Echo(Box::new(Type::Dist(Box::new(Type::Int))))
17191731
);
17201732
}
17211733

compiler/bet-codegen/src/lib.rs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,24 +102,36 @@ pub fn codegen_module(module: &Module, target: Target) -> CompileResult<CodeOutp
102102
fn js_preamble() -> &'static str {
103103
r#"// === Betlang Runtime Preamble ===
104104
105-
// Uniform ternary choice among exactly 3 alternatives
105+
// Uniform ternary choice among exactly 3 alternatives.
106+
// `bet` has type `Dist T`: it returns a *distribution object* (sample with
107+
// `__bet_sample`), not an eager draw — consistent with the checker/interpreter.
106108
function __bet_uniform(a, b, c) {
107-
const r = Math.random();
108-
if (r < 1/3) return a;
109-
if (r < 2/3) return b;
110-
return c;
109+
return {
110+
name: 'bet',
111+
sample() {
112+
const r = Math.random();
113+
if (r < 1/3) return a;
114+
if (r < 2/3) return b;
115+
return c;
116+
},
117+
};
111118
}
112119
113-
// Weighted ternary choice (weights normalised internally)
120+
// Weighted ternary choice (weights normalised internally). Returns a Dist.
114121
function __bet_weighted(alts, weights) {
115-
const total = weights.reduce((s, w) => s + w, 0);
116-
const r = Math.random() * total;
117-
let cumul = 0;
118-
for (let i = 0; i < alts.length; i++) {
119-
cumul += weights[i];
120-
if (r < cumul) return alts[i];
121-
}
122-
return alts[alts.length - 1];
122+
return {
123+
name: 'weighted_bet',
124+
sample() {
125+
const total = weights.reduce((s, w) => s + w, 0);
126+
const r = Math.random() * total;
127+
let cumul = 0;
128+
for (let i = 0; i < alts.length; i++) {
129+
cumul += weights[i];
130+
if (r < cumul) return alts[i];
131+
}
132+
return alts[alts.length - 1];
133+
},
134+
};
123135
}
124136
125137
// --- Distribution objects ---

compiler/bet-eval/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ license.workspace = true
88
[dependencies]
99
bet-syntax = { path = "../bet-syntax" }
1010
bet-core = { path = "../bet-core" }
11+
bet-rt = { path = "../../runtime/bet-rt" } # the unified runtime Value (Dist, …)
12+
bet-rand = { path = "../../runtime/bet-rand" } # centralised ternary/weighted RNG
13+
im.workspace = true # bet_rt::Value::List is im::Vector
1114
rand.workspace = true
1215
rand_distr.workspace = true
1316
thiserror.workspace = true

0 commit comments

Comments
 (0)