A fast, embeddable reasoning engine in C99. DriftNARS implements Non-Axiomatic Logic (NAL) levels 1-8 — temporal reasoning, uncertainty handling, learning from experience, and goal-driven decision making — in a single-file-includable C library with no external dependencies.
Unlike neural networks, NARS reasons symbolically with explicit logic: it deduces new knowledge from what it's told, learns cause-and-effect from observation, and takes actions to achieve goals — all with built-in uncertainty tracking. Every conclusion carries a truth value that says how much evidence supports it.
Forked from OpenNARS for Applications (ONA)
at commit dc4efd0,
then refactored into a clean embeddable library core.
- Build
- Quick Start
- DriftScript
- Narsese Shell
- C Library
- Python
- HTTP Server
- State Persistence
- Memory and Resource Management
- Error Handling
- Documentation
- License
make # builds bin/driftnars + .a + .dylib/.so
make OPENMP=1 # with OpenMP threading
make test # run all unit + system tests
make clean # remove build artifactsTwo-stage build: Stage 1 compiles a bootstrap binary that generates the inference
rule table (src/engine/RuleTable.c), then Stage 2 compiles the final binary into bin/.
make
bin/driftnars driftscriptdriftscript> (believe (inherit "robin" "bird"))
driftscript> (believe (inherit "bird" "animal"))
driftscript> (ask (inherit "robin" "animal"))
Answer: <robin --> animal>. ... Truth: frequency=1.000000, confidence=0.810000
You give the system facts and questions — it reasons out the answers. We never said robin is an animal; it deduced that from the two inheritance links. The confidence of 0.81 (vs the input's 0.9) reflects that this is an indirect conclusion — the system tracks evidence strength automatically.
DriftScript is DriftNARS's human-friendly input language. It compiles to Narsese (the underlying logic language) but replaces angle brackets and cryptic copula symbols with readable S-expressions:
; Teach the system some facts
(believe (inherit "bird" "animal"))
(believe (inherit "robin" "bird"))
; Ask a question — the system deduces the answer
(ask (inherit "robin" "animal"))
; Goal-driven action: teach a rule, provide a state, set a goal — the system acts
(def-op ^press)
(believe (predict (seq "light_on" (call ^press)) "light_off"))
(believe "light_on" :now)
(goal "light_off")
; => ^press executedConcept names are quoted strings. Keywords (believe, inherit, seq), variables
($x, ?what), and operations (^press) stay unquoted.
bin/driftnars driftscriptThe DriftScript REPL compiles input on the fly and feeds it directly to the reasoner.
Multi-line input is supported — the prompt changes to ...> while parentheses are
unbalanced. Line editing, history, and Ctrl-C/Ctrl-D work as expected. Type quit
to exit.
Piped input and scripts work too — the prompt is suppressed automatically:
bin/driftnars driftscript < examples/driftscript/01_hello.dsThe examples/driftscript/ directory contains 10 progressive
tutorial files, each self-contained and runnable. Start from zero and work through
deduction, temporal reasoning, learning from experience, and multi-step planning:
| File | Topic |
|---|---|
01_hello.ds |
First beliefs, questions, deduction |
02_truth.ds |
Truth values: frequency, confidence, expectation |
03_copulas.ds |
All 6 relationship types |
04_connectors.ds |
Compound terms: sets, products, sequences |
05_time.ds |
Temporal reasoning and predictions |
06_operations.ds |
Goals, actions, and decision making |
07_variables.ds |
Universal, existential, and query variables |
08_learning.ds |
Learning rules from observation |
09_multistep.ds |
Multi-step planning and goal decomposition |
10_config.ds |
Tuning: volume, thresholds, cycles, reset |
bin/driftnars driftscript < examples/driftscript/01_hello.dsDriftScript compiles to Narsese. You can use either — DriftScript is more readable, Narsese is more compact:
| Narsese | DriftScript |
|---|---|
<bird --> animal>. |
(believe (inherit "bird" "animal")) |
<robin --> animal>? |
(ask (inherit "robin" "animal")) |
light_off! :|: |
(goal "light_off") |
<($1 --> bird) ==> ($1 --> animal)>. |
(believe (imply (inherit $x "bird") (inherit $x "animal"))) |
<(light_on &/ ^press) =/> light_off>. |
(believe (predict (seq "light_on" (call ^press)) "light_off")) |
For direct Narsese input — the raw logic language without the DriftScript layer:
bin/driftnars shelldriftnars> <bird --> animal>.
driftnars> <robin --> bird>.
driftnars> <robin --> animal>?
Answer: <robin --> animal>. creationTime=2 Truth: frequency=1.000000, confidence=0.810000
The Narsese shell has full line editing (arrow keys, Home/End, backspace), a 32-entry command history (up/down arrows), Ctrl-C to clear the line, and Ctrl-D to exit. Piped input and scripts work automatically with prompt suppression:
echo '<bird --> animal>.' | bin/driftnars shellType help at the prompt for a summary of all Narsese input formats, tense markers,
truth value syntax, and * commands (e.g., *volume=0, *motorbabbling=0.1).
Both shells share the same reasoning engine and the same line editing — choose DriftScript for readability or Narsese for directness.
DriftNARS compiles to a static library (libdriftnars.a) and a shared library
(libdriftnars.dylib/.so) for embedding in any C/C++ application:
#include "NAR.h"
NAR_t *nar = NAR_New();
NAR_INIT(nar);
NAR_AddInputNarsese(nar, "<bird --> animal>.");
NAR_AddInputNarsese(nar, "<robin --> bird>.");
NAR_AddInputNarsese(nar, "<robin --> animal>?");
NAR_Cycles(nar, 100);
NAR_Free(nar);Link with -lm -lpthread.
All mutable state lives in the NAR_t struct — no hidden globals — so you can run
multiple independent reasoner instances in the same process.
Instead of parsing stdout, register structured callbacks for answers, decisions, operation execution, and inference events:
void my_answer(void *ud, const char *narsese, double freq, double conf,
long occTime, long createTime) {
printf("Got answer: %s f=%.2f c=%.2f\n", narsese, freq, conf);
}
NAR_SetAnswerHandler(nar, my_answer, NULL);Four callback types: NAR_SetEventHandler, NAR_SetAnswerHandler,
NAR_SetDecisionHandler, NAR_SetExecutionHandler. All are optional and
use flat C primitives for easy FFI integration.
A ctypes wrapper provides the full DriftNARS API from Python, with both Narsese and DriftScript input:
from driftnars import DriftNARS
with DriftNARS() as nar:
nar.on_answer(lambda n, f, c, occ, ct: print(f"Answer: {n} f={f:.2f} c={c:.2f}"))
nar.on_execution(lambda op, args: print(f"{op} executed"))
# DriftScript — the readable way
nar.add_driftscript("""
(def-op ^press)
(believe (predict (seq "light_on" (call ^press)) "light_off"))
(believe "light_on" :now)
(goal "light_off")
""")
# Or raw Narsese
nar.add_narsese("<bird --> animal>.")See examples/python/ for complete examples with all four
callback types.
DriftNARS includes a lightweight HTTP server for integrating the reasoning engine with web applications, scripts, or any HTTP client:
make httpd
bin/driftnars-httpd --port 8080| Method | Endpoint | Description |
|---|---|---|
POST |
/driftscript |
Compile & execute DriftScript, returns engine output |
POST |
/narsese |
Execute raw Narsese / shell commands (one per line) |
POST |
/reset |
Reset the reasoner |
GET |
/health |
Liveness check — returns {"status":"ok"} |
GET |
/ops |
List registered operations and their callback URLs |
POST |
/ops/register |
Register an operation with a callback URL |
DELETE |
/ops/:name |
Unregister an operation |
POST |
/config |
Set runtime reasoner parameters |
POST |
/save |
Save entire state to binary file |
POST |
/load |
Load state from binary file |
POST |
/compact |
Free lowest-priority concepts to reduce memory |
curl -X POST http://127.0.0.1:8080/driftscript -d '
(believe (inherit "robin" "bird"))
(believe (inherit "bird" "animal"))
(cycles 5)
(ask (inherit "robin" "animal"))
'Register operations at runtime. When the reasoner decides to execute an operation, the server sends an HTTP POST to the registered callback URL with a JSON payload:
# Register ^press — when executed, POST to your service
curl -X POST http://127.0.0.1:8080/ops/register \
-H 'Content-Type: application/json' \
-d '{
"op": "^press",
"callback_url": "http://localhost:4000/nars/executions",
"min_confidence": 0.6
}'
# Teach a rule and trigger the operation
curl -X POST http://127.0.0.1:8080/driftscript -d '
(believe (predict (seq "light_on" (call ^press)) "light_off"))
(believe "light_on" :now)
(goal "light_off")
'
# => ^press fires, your service receives:
# {"op":"^press","args":"","frequency":1.0,"confidence":1.0,"timestamp_ms":...}# List registered operations
curl http://127.0.0.1:8080/ops
# Unregister
curl -X DELETE http://127.0.0.1:8080/ops/^presscurl -X POST http://127.0.0.1:8080/config \
-H 'Content-Type: application/json' \
-d '{
"decision_threshold": 0.65,
"motorbabbling": 0.0,
"volume": 0
}'Available config keys: decision_threshold, motorbabbling, volume,
anticipation_confidence, question_priming.
Save and restore the entire reasoner state via the HTTP API — see the State Persistence section below for full details including CLI usage, the C API, and what gets serialized.
See examples/httpd/ for ready-to-run scripts:
./examples/httpd/example.sh # demo all endpoints
./examples/httpd/test_ops.sh # test operation callback API + save/load (20 tests)DriftNARS can save its entire state — all learned concepts, beliefs, temporal
implications, event queues, configuration, and timing — to a binary .dnar file,
and reload it later. This lets you checkpoint a trained system, shut down, and
resume exactly where you left off.
Everything the reasoner has learned and every tunable parameter:
- All concepts and their beliefs, goal spikes, predicted beliefs
- Temporal implication tables (precondition beliefs, implication links)
- Cycling belief and goal event queues with priorities
- Atom table (all term names the system has seen)
- Occurrence time index
- Operation registrations (names and babbling arguments, but not C function pointers)
- Runtime parameters (decision threshold, motor babbling, truth decay, etc.)
- Internal counters (current time, stamp base, RNG seed, concept IDs)
What is not saved: C function pointers (operation callbacks, output handlers). These belong to the running process and are preserved across a load — you don't need to re-register them.
From either the Narsese shell or DriftScript REPL:
driftnars> <bird --> animal>.
driftnars> <robin --> bird>.
driftnars> 5
driftnars> *save /tmp/brain.dnar
State saved to /tmp/brain.dnar
driftnars> *reset
driftnars> *load /tmp/brain.dnar
State loaded from /tmp/brain.dnar
driftnars> <robin --> animal>?
Answer: <robin --> animal>. creationTime=2 Truth: frequency=1.000000, confidence=0.810000
driftnars> *compact 50
Compacted to 50 concepts (50 allocated)
This also works with piped input for scripted workflows:
echo '<bird --> animal>.
<robin --> bird>.
5
*save /tmp/brain.dnar' | bin/driftnars shell# Save current state
curl -X POST http://127.0.0.1:8080/save \
-H 'Content-Type: application/json' \
-d '{"path":"/tmp/brain.dnar"}'
# => {"status":"saved","path":"/tmp/brain.dnar"}
# Load state (replaces current state entirely)
curl -X POST http://127.0.0.1:8080/load \
-H 'Content-Type: application/json' \
-d '{"path":"/tmp/brain.dnar"}'
# => {"status":"loaded","path":"/tmp/brain.dnar"}
# Free lowest-priority concepts to reduce memory
curl -X POST http://127.0.0.1:8080/compact \
-H 'Content-Type: application/json' \
-d '{"target":100}'
# => {"status":"compacted","concepts":100,"allocated":100}After loading, the system continues reasoning from the restored state. Any registered HTTP operation callbacks survive the load automatically.
// Save
int rc = NAR_Save(nar, "/tmp/brain.dnar"); // returns NAR_OK or NAR_ERR_IO
// Load (replaces all state in nar)
rc = NAR_Load(nar, "/tmp/brain.dnar"); // returns NAR_OK or NAR_ERR_IO
// Free lowest-priority concepts to reduce memory (returns remaining count)
int remaining = NAR_Compact(nar, 100);Output callbacks (NAR_SetAnswerHandler, etc.) and operation action function
pointers are preserved across NAR_Load — they are not part of the file.
The binary format includes a header with all compile-time configuration constants
(CONCEPTS_MAX, ATOMS_MAX, STAMP_SIZE, etc.). A .dnar file can only be loaded
by a binary compiled with the same configuration. Attempting to load a file from an
incompatible build returns NAR_ERR_IO.
DriftNARS uses lazy, on-demand allocation for concepts. When you call NAR_New(),
it allocates a NAR_t struct (~6 MB) containing event queues, hash tables, and index
structures — but no concepts. Each Concept (~294 KB, due to its implication tables
with TABLE_SIZE=120 entries) is allocated individually from the heap only when the
reasoner first needs it.
A fresh instance with 3 inputs uses ~8 MB total. At full capacity (CONCEPTS_MAX=4096
concepts), memory usage reaches ~1.2 GB — the same capacity as ONA, but you only pay
for what you use.
This design is deliberate:
- Pay-as-you-go — memory grows with actual usage, not maximum capacity
- Same reasoning characteristics — identical
TABLE_SIZEandCONCEPTS_MAXas the original, preserving probability distributions and inference quality - Bounded — the system never exceeds its configured maximum, and when concept capacity is reached, lowest-priority concepts are evicted and their storage recycled
Key limits are compile-time constants in src/engine/Config.h:
| Parameter | Default | What it bounds |
|---|---|---|
CONCEPTS_MAX |
4096 | Maximum concepts in memory |
ATOMS_MAX |
255 | Maximum distinct atom names |
OPERATIONS_MAX |
10 | Maximum registered operations |
CYCLING_BELIEF_EVENTS_MAX |
20 | Belief event cycling queue size |
CYCLING_GOAL_EVENTS_MAX |
10 | Goal event cycling queue size |
STAMP_SIZE |
10 | Evidential base entries per stamp |
TABLE_SIZE |
120 | Implications per concept slot |
When a pool is full, the system handles it gracefully — it evicts the lowest-priority item (for priority queues) or silently drops the new entry (for index structures). This is NARS's "Attention and Resource Control" (AIKR) principle: finite resources force the system to prioritize, which is a feature, not a limitation.
To change limits, edit src/engine/Config.h and rebuild. Increasing CONCEPTS_MAX
increases memory proportionally (~300 KB per concept with default TABLE_SIZE).
Reducing TABLE_SIZE or OPERATIONS_MAX significantly reduces per-concept size.
Note that .dnar save files are tied to the compile-time configuration — you cannot
load a file saved with different limits.
All internal data structure operations fail gracefully rather than aborting:
- Stack overflow/underflow —
Stack_Pushreturnsfalsewhen full,Stack_PopreturnsNULLwhen empty. Callers check and degrade safely (e.g., the inverted atom index silently skips indexing if the pool is exhausted). - Hash table full —
HashTable_Setsilently drops the entry when the internal free list is empty.HashTable_Deleteis a no-op if the item isn't found. - Narsese input too long — returns
NULL/ empty term instead of aborting. The parser remains usable for subsequent normal-length input. - Atom table full — returns atom index 0 (invalid) when
ATOMS_MAXis exceeded. - Memory allocation failure —
NAR_New()returnsNULLifcallocfails. - File I/O errors —
NAR_Save/NAR_LoadreturnNAR_ERR_IOon failure.
Error codes returned by the public API:
| Code | Constant | Meaning |
|---|---|---|
| 0 | NAR_OK |
Success |
| -1 | NAR_ERR_PARSE |
Narsese parse or input format error |
| -2 | NAR_ERR_MEM |
Memory full — input dropped (non-fatal) |
| -3 | NAR_ERR_INIT |
Called before NAR_INIT |
| -4 | NAR_ERR_IO |
File I/O error (save/load) |
The system is designed to keep running when it hits limits. Concepts get evicted by priority, events cycle through bounded queues, and evidence accumulates within fixed-size stamps. These aren't error conditions — they're the normal operating mode of a resource-bounded reasoning system.
| Document | Description |
|---|---|
docs/driftscript_reference.md |
DriftScript language reference |
docs/narsese_primer.md |
Narsese language reference |
examples/driftscript/ |
DriftScript tutorial (10 progressive lessons) |
examples/python/ |
Python integration examples |
engineering_log.md |
Change history from the ONA fork |
MIT. See LICENSE.