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
4 changes: 3 additions & 1 deletion bin/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#
# Engine options:
# --dispatch-cap <N|off> --lib-depth <N> --jdk-depth <N> --taint <on|off>
# --closed-world <on|off> narrow the dispatch fan to constructed types (TypeScript), recording
# each dropped edge as an assumption row
#
# Commands:
# parser <src-dir> <ir-dir> [--version V] [--exclude-tests]
Expand Down Expand Up @@ -195,7 +197,7 @@ case "$cmd" in
--language) lang="$2"; shift 2;; --src) src="$2"; shift 2;; --out) out="$2"; shift 2;;
--library) libs="$2"; shift 2;; --progress) progress="$2"; shift 2;;
--version|--slug) version="$2"; shift 2;; --exclude-tests) popts+=(--exclude-tests); shift;;
--intermediate|--dispatch-cap|--lib-depth|--jdk-depth|--taint) rest+=("$1" "$2"); shift 2;;
--intermediate|--dispatch-cap|--lib-depth|--jdk-depth|--taint|--closed-world) rest+=("$1" "$2"); shift 2;;
--debug) rest+=(--debug); shift;;
--*) die "unknown option $1 (try --help)";;
*) pos+=("$1"); shift;; esac; done
Expand Down
1 change: 1 addition & 0 deletions graph/bundle/SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ What produced this bundle: one key/value row per fact about the run (language, e
| `client_ir` | all | Path of the client IR directory the engine read. |
| `library_roots` | all | Comma-separated library IR roots staged as the type oracle; empty for a client-only run. |
| `dispatch_cap` | all | Fan-width cap on virtual dispatch in effect; `off` when uncapped. |
| `dispatch_closed_world` | all | `on` when the dispatch fan was narrowed to types the analysed code constructs (a closed-world premise; each dropped edge is an assumption row); `off` otherwise. Read by TypeScript. |
| `jdk_depth` | all | Platform-library hop cap (engine-ii). |
| `lib_depth` | all | External-library hop cap; `uncapped` when unset. |
| `engine_ii` | all | `on` when the library-frontier forward chain (engine-ii) was included; `off` for a client-only solve. |
Expand Down
1 change: 1 addition & 0 deletions graph/bundle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export const VOCAB: readonly VocabSpec[] = [
{ table: 'run', column: 'key', value: 'client_ir', languages: 'all', meaning: 'Path of the client IR directory the engine read.' },
{ table: 'run', column: 'key', value: 'library_roots', languages: 'all', meaning: 'Comma-separated library IR roots staged as the type oracle; empty for a client-only run.' },
{ table: 'run', column: 'key', value: 'dispatch_cap', languages: 'all', meaning: 'Fan-width cap on virtual dispatch in effect; `off` when uncapped.' },
{ table: 'run', column: 'key', value: 'dispatch_closed_world', languages: 'all', meaning: '`on` when the dispatch fan was narrowed to types the analysed code constructs (a closed-world premise; each dropped edge is an assumption row); `off` otherwise. Read by TypeScript.' },
{ table: 'run', column: 'key', value: 'jdk_depth', languages: 'all', meaning: 'Platform-library hop cap (engine-ii).' },
{ table: 'run', column: 'key', value: 'lib_depth', languages: 'all', meaning: 'External-library hop cap; `uncapped` when unset.' },
{ table: 'run', column: 'key', value: 'engine_ii', languages: 'all', meaning: '`on` when the library-frontier forward chain (engine-ii) was included; `off` for a client-only solve.' },
Expand Down
1 change: 1 addition & 0 deletions graph/csharp/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// ============================================================================

.decl dispatch_cap(c0:symbol)
.decl dispatch_closed_world(c0:symbol)
.decl jdk_max_depth(c0:number)
.decl lib_max_depth(c0:number)
.decl runtime_observed_edge(c0:symbol,c1:symbol,c2:symbol)
Expand Down
3 changes: 3 additions & 0 deletions graph/csharp/souffle/gen_decls_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
# --dispatch-cap K: refuse a virtual-dispatch fan wider than K. Recall-risky, so
# opt-in; empty means uncapped, which is the answer that cannot be wrong.
"dispatch_cap": ("symbol",),
# --closed-world on: narrow the dispatch fan to constructed types (#473). Staged
# for every language by the shared executor; no C# rule reads it yet.
"dispatch_closed_world": ("symbol",),
# --lib-depth: how far a client->lib chain is expanded. Empty means uncapped.
"lib_max_depth": ("number",),
# The shared executor ALWAYS stages these two, for every language, and emits one
Expand Down
1 change: 1 addition & 0 deletions graph/java/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@
.decl lib_dispatch_width_low(c0:symbol,c1:number)
.decl lib_wide_dispatch(c0:symbol)
.decl dispatch_cap(c0:symbol)
.decl dispatch_closed_world(c0:symbol)
.decl field_final(c0:symbol)
.decl field_init_new_type(c0:symbol,c1:symbol)
.decl field_multi_new(c0:symbol)
Expand Down
1 change: 1 addition & 0 deletions graph/javascript/souffle/decls_all.dl
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@
.decl literal_contains_method_nested(c0:symbol, c1:symbol)
// ── executor knobs (written by run-souffle.sh for every language; unread here) ──
.decl dispatch_cap(c0:symbol)
.decl dispatch_closed_world(c0:symbol)
.decl jdk_max_depth(c0:number)
.decl lib_max_depth(c0:number)
.decl taint_gating(c0:symbol)
Expand Down
23 changes: 19 additions & 4 deletions graph/pipeline/run-souffle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ DISPATCH_CAP="${DISPATCH_CAP:-20}" # fan-width cap on virtual dispatch. DEFAUL
LANG_ARG="" # which rule set under graph/<lang>/ to run. Default java.
TAINT="" # --taint on → gate lib→lib GROW on client-seeded data flow (dataflow/taint.dl). Also
# settable via env AXIOM_TAINT_GATING=on. Empty = ungated (default behavior).
MODE="run" # run | print-engine-id | emit-program — the last two need no IR and no souffle
CLOSED_WORLD="" # --closed-world on → narrow the dispatch fan to types the program constructs (RTA),
# and record every edge that drops as an assumption row. Env AXIOM_DISPATCH_CLOSED_WORLD=on.
# Empty = the fan is every declared override (default). See #473.
MODE="run" # run | print-engine-id | emit-program — the last two need no IR and no souffle
EMIT=""
while [ $# -gt 0 ]; do case "$1" in
--client-ir) CLIENT="$2"; shift 2;; --library) LIB="$2"; shift 2;;
Expand All @@ -54,6 +57,7 @@ while [ $# -gt 0 ]; do case "$1" in
--dispatch-cap) DISPATCH_CAP="$2"; shift 2;;
--lib-depth) LIB_DEPTH="$2"; shift 2;;
--taint) TAINT="$2"; shift 2;;
--closed-world) CLOSED_WORLD="$2"; shift 2;;
--language) LANG_ARG="$2"; shift 2;;
--print-engine-id) MODE="print-engine-id"; shift;;
--emit-program) MODE="emit-program"; EMIT="$2"; shift 2;;
Expand Down Expand Up @@ -195,7 +199,7 @@ write_program(){
map_rels_su "$TPL/client-ir.map" || return 1
map_rels_su "$TPL/lib.map" sig || return 1
for r in $LIB_BODY; do _SU+=("$r"); done
_SU+=(jdk_max_depth lib_max_depth taint_gating dispatch_cap)
_SU+=(jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world)
sort_unique_su
PROGRAM_INPUTS=(${_SU[@]+"${_SU[@]}"})
# rfc4180=true: the IR is CSV, not TSV. The parser quotes any field containing a
Expand Down Expand Up @@ -247,7 +251,7 @@ write_program(){
# the expected one, each exactly once, with nothing extra. Exit status only: no pipe to lose.
verify_program(){
awk -v cmap="$TPL/client-ir.map" -v lmap="$TPL/lib.map" -v man="$DL/export_manifest.tsv" \
-v libsig=" $LIB_SIG " -v extra="$LIB_BODY jdk_max_depth lib_max_depth taint_gating dispatch_cap" '
-v libsig=" $LIB_SIG " -v extra="$LIB_BODY jdk_max_depth lib_max_depth taint_gating dispatch_cap dispatch_closed_world" '
function want(line) { if (!(line in need)) { need[line] = 1; n++ } }
function inp(r) { want(".input " r "(IO=file, filename=\"" r ".facts\", delimiter=\"\\t\", rfc4180=true)") }
function maprels(path, sigonly, l, rc, r, k) {
Expand Down Expand Up @@ -504,6 +508,17 @@ echo "▶ taint gating = $( [ -s "$FACTS/taint_gating.facts" ] && echo 'on (opt-
CAP_EFF="${AXIOM_DISPATCH_CAP:-$DISPATCH_CAP}"
case "$CAP_EFF" in off|none|no|0|"") CAP_EFF="";; esac
[ -n "$CAP_EFF" ] && printf '%s\n' "$CAP_EFF" > "$FACTS/dispatch_cap.facts"

# dispatch_closed_world — OPT-IN RTA narrowing of the dispatch fan (#473). The file always exists
# so its .input directive is generated; EMPTY = the fan is every declared override (the default,
# and what every golden pins). "on" admits only overrides a constructed or escaping type can
# reach, and exports each dropped edge as an assumption row: the narrowing is sound only when
# every construction is in the analysed code, so the bundle says it was made. Only front ends
# that read the relation change; the others ignore it.
: > "$FACTS/dispatch_closed_world.facts"
CW_EFF="${AXIOM_DISPATCH_CLOSED_WORLD:-$CLOSED_WORLD}"
case "$CW_EFF" in on|yes|1|true) CW_EFF="on"; printf 'on\n' > "$FACTS/dispatch_closed_world.facts";; *) CW_EFF="off";; esac
echo "▶ dispatch closed world = $CW_EFF"
echo "▶ dispatch cap = $( [ -s "$FACTS/dispatch_cap.facts" ] && echo "$(cat "$FACTS/dispatch_cap.facts") (default; --dispatch-cap off for unbounded reachability)" || echo 'OFF — uncapped/sound (sink & taint traversal)' )"

# engine-ii (lib-frontier forward-chain) is GATED — default OFF so the build is CLIENT-ONLY
Expand Down Expand Up @@ -803,7 +818,7 @@ BUNDLE_FLAGS=(); [ "$DEBUG_BUNDLE" = "1" ] && BUNDLE_FLAGS+=(--debug)
"${BUNDLE[@]}" --language "$LANG_ARG" --src "$SRC" --client-ir "$CLIENT" --raw "$RAW" --out "$OUT" \
--library "$LIB" --lib-facts "$LIBDIR" "${BUNDLE_FLAGS[@]}" \
--meta "engine_commit=$ENGINE_COMMIT" \
--meta "dispatch_cap=${CAP_EFF:-off}" --meta "jdk_depth=$JDK_DEPTH" --meta "lib_depth=${LIB_DEPTH:-uncapped}" \
--meta "dispatch_cap=${CAP_EFF:-off}" --meta "dispatch_closed_world=$CW_EFF" --meta "jdk_depth=$JDK_DEPTH" --meta "lib_depth=${LIB_DEPTH:-uncapped}" \
--meta "engine_ii=$ENGINE_II_MODE" --meta "solve_iterations=$iter" --meta "solve_seconds=$((SOLVE_EPOCH-START_EPOCH))" \
${EXTRA_META[@]+"${EXTRA_META[@]}"}

Expand Down
1 change: 1 addition & 0 deletions graph/python/souffle/decls_all.dl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.decl dispatch_cap(c0:symbol)
.decl dispatch_closed_world(c0:symbol)
.decl jdk_max_depth(c0:number)
.decl lib_max_depth(c0:number)
.decl taint_gating(c0:symbol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// A call through an interface-typed receiver fans to every declared implementor by
// default. `--closed-world on` narrows that fan to what the program can build (#473).
// Each class below is one shape of "can the program build it":

export interface Handler {
handle(x: string): string;
}

// constructed with `new`: stays in the narrowed fan
export class Built implements Handler {
handle(x: string): string { return "b" + x; }
}

// never written in a `new`, but handed to a registry as a class VALUE that constructs it:
// stays, because a class that escapes as a value can be built where the parser cannot see
export class Registered implements Handler {
handle(x: string): string { return "r" + x; }
}

// named only as a TYPE: nothing can build it, so the narrowed fan drops it
export class Unused implements Handler {
handle(x: string): string { return "u" + x; }
}

// never constructed itself, but a constructed subclass inherits its body: stays
export abstract class Base implements Handler {
handle(x: string): string { return "base" + x; }
}
export class Leaf extends Base {}

// a subclass nobody constructs, overriding: dropped
export class Orphan extends Base {
handle(x: string): string { return "o" + x; }
}

type HandlerClass = new () => Handler;
const registry: HandlerClass[] = [];
export function register(c: HandlerClass): void { registry.push(c); }
register(Registered);

export function dispatch(h: Handler, x: string): string {
return h.handle(x);
}

export function main(): void {
const built = new Built();
const leaf = new Leaf();
let later: Unused | undefined;
let other: Orphan | undefined;
dispatch(built, "1");
dispatch(leaf, "2");
for (const C of registry) dispatch(new C(), "3");
void later;
void other;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Handler } from "./handlers";

// declared in one module, handed to a container as a class VALUE from another
// (`useClass: Provided` in module.ts): stays in the narrowed fan
export class Provided implements Handler {
handle(x: string): string { return "p" + x; }
}

// imported by module.ts too, but only ever named there as a TYPE: dropped
export class TypeOnly implements Handler {
handle(x: string): string { return "t" + x; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Provided, TypeOnly } from "./impl";

// The container shape: the class crosses a module boundary as an imported value and is
// constructed by code the analysis does not see.
export const providers = [{ provide: "handler", useClass: Provided }];

export let pending: TypeOnly | undefined;
16 changes: 16 additions & 0 deletions graph/test/typescript/expected/83-closed-world-dispatch.edges
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ambiguous_unknown CONSTRUCTOR_CALL handlers#main() @L52 -> -
ambiguous_unknown METHOD_CALL handlers#register(HandlerClass) @L38 -> -
known_edge CONSTRUCTOR_CALL handlers#main() @L46 -> Built#<new>()
known_edge CONSTRUCTOR_CALL handlers#main() @L47 -> Base#<new>()
known_edge FUNCTION_CALL handlers#<module-init>() @L39 -> handlers#register(HandlerClass)
known_edge FUNCTION_CALL handlers#main() @L50 -> handlers#dispatch(Handler,string)
known_edge FUNCTION_CALL handlers#main() @L51 -> handlers#dispatch(Handler,string)
known_edge FUNCTION_CALL handlers#main() @L52 -> handlers#dispatch(Handler,string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Base#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Built#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Handler#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Orphan#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Provided#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Registered#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> TypeOnly#handle(string)
multi_inferred METHOD_CALL handlers#dispatch(Handler,string) @L42 -> Unused#handle(string)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
── entry_point (1) ──
unimported_module module#<module> module.ts:1
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
nominal handlers#Base.handle -> handlers#Orphan.handle
nominal handlers#Handler.handle -> handlers#Base.handle
nominal handlers#Handler.handle -> handlers#Built.handle
nominal handlers#Handler.handle -> handlers#Orphan.handle
nominal handlers#Handler.handle -> handlers#Registered.handle
nominal handlers#Handler.handle -> handlers#Unused.handle
nominal handlers#Handler.handle -> impl#Provided.handle
nominal handlers#Handler.handle -> impl#TypeOnly.handle
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
83-closed-world-dispatch [fields]
precision 0.0000 (0 correct, 0 wrong)
recall 0.0000 (0 of 0 the compiler resolved)
sites 0 resolved 0
tiers
access
not scored: 0 rows whose target is not a client declaration
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Accepted gaps for 83-closed-world-dispatch — each line is an edge the TypeScript compiler
# resolves and this engine does not. A NEW missing edge fails the suite;
# a line here that STARTS working also fails, so the debt cannot rot.
# `new C()` where C is typed by a construct-signature alias: the compiler names the
# alias's signature, the engine leaves the site ambiguous_unknown. Unrelated to the
# closed-world fan this case is about.
handlers#main() -> handlers#<arrow@36>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
oracle=6 engine=12 agree=5 missing=1 (known 1, NEW 0) extra=7
known handlers#main() -> handlers#<arrow@36>()
extra handlers#dispatch(Handler,string) -> Base#handle(string)
extra handlers#dispatch(Handler,string) -> Built#handle(string)
extra handlers#dispatch(Handler,string) -> Orphan#handle(string)
extra handlers#dispatch(Handler,string) -> Provided#handle(string)
extra handlers#dispatch(Handler,string) -> Registered#handle(string)
extra handlers#dispatch(Handler,string) -> TypeOnly#handle(string)
extra handlers#dispatch(Handler,string) -> Unused#handle(string)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ambiguous_unknown OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> -
known_edge IMPLEMENTS_INTERFACE 0 Base [HERITAGE] -> Handler
known_edge IMPLEMENTS_INTERFACE 0 Built [HERITAGE] -> Handler
known_edge IMPLEMENTS_INTERFACE 0 Provided [HERITAGE] -> Handler
known_edge IMPLEMENTS_INTERFACE 0 Registered [HERITAGE] -> Handler
known_edge IMPLEMENTS_INTERFACE 0 TypeOnly [HERITAGE] -> Handler
known_edge IMPLEMENTS_INTERFACE 0 Unused [HERITAGE] -> Handler
known_edge METHOD_PARAM 0 handlers [METHOD_PARAM] -> Handler
known_edge METHOD_PARAM 0 handlers [METHOD_PARAM] -> HandlerClass
known_edge METHOD_RETURN 1 HandlerClass [TYPE] -> Handler
known_edge OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> Built
known_edge OBJECT_CREATION_TYPE 0 handlers [EXPRESSION] -> Leaf
known_edge SUPER_TYPE 0 Leaf [HERITAGE] -> Base
known_edge SUPER_TYPE 0 Orphan [HERITAGE] -> Base
known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> HandlerClass
known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> Orphan
known_edge TYPE_ELEMENT 1 handlers [VARIABLE] -> Unused
known_edge TYPE_ELEMENT 1 module [VARIABLE] -> TypeOnly
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
83-closed-world-dispatch [types]
precision 1.0000 (16 correct, 0 wrong)
recall 1.0000 (16 of 16 the compiler resolved)
sites 18 resolved 17 (94.4%)
tiers ambiguous_unknown=1 known_edge=17
contexts IMPLEMENTS_INTERFACE=6 METHOD_PARAM=2 METHOD_RETURN=1 OBJECT_CREATION_TYPE=3 SUPER_TYPE=2 TYPE_ELEMENT=4
not scored: 1 rows whose target is not a client declaration
8 changes: 8 additions & 0 deletions graph/test/typescript/run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,14 @@ if ! bash "$HERE/tools/envelope-merge-test.sh"; then
exit 1
fi
PARSER="${AXIOM_PARSER:-$ROOT/parser/dist/index.js}"
# ── --closed-world on narrows the dispatch fan and records it (#473) ─────────
# The goldens pin the default (CHA) fan; this solves case 82 with the flag on and checks
# that exactly the never-constructed implementors drop, each with an assumption row, and
# that a class built through `new`, through a registry, or through a subclass stays.
if ! bash "$HERE/tools/closed-world-dispatch-test.sh"; then
echo "aborting: --closed-world does not narrow the fan the way it claims"
exit 1
fi
WORK="$HERE/.work"
# shellcheck source=../tools/case-pool.sh
. "$ROOT/graph/test/tools/case-pool.sh"
Expand Down
Loading
Loading