Started working on reduction system

This commit is contained in:
2026-04-19 21:05:10 +02:00
parent e12b55114e
commit 0376aab672
6 changed files with 402 additions and 7 deletions

View File

@@ -0,0 +1,14 @@
import { Reduction_Scanner, Reduction_Settings } from '@efforting.tech/rule-processing/reduction-scanner';
//import { Sub_Sequence_Rule } from '@efforting.tech/rule-processing/rules';
const rss = Reduction_Settings.load();
const rs = new Reduction_Scanner(rss);
rss.rules.push();
const arr = [10, '+', 20, '*', 30];
console.log(rs.perform_reduction(arr));
console.log(arr);

View File

@@ -0,0 +1,74 @@
import { FPR_State } from './state.mjs';
export class Abstract_Reduction_Contract {
on_create_scanner(scanner) {}
on_start_transform(scanner, sequence) {}
on_end_transform(scanner, sequence) {}
on_reduction_attempted(scanner, sequence) {}
on_reduction_made(scanner, sequence) {}
check_if_done(scanner, sequence) {
return false;
}
assert_readiness(scanner, sequence) {}
}
export class Reduction_Contract extends Abstract_Reduction_Contract {
check_if_done(scanner, sequence) {
return scanner.last_reduction_status === false;
}
}
export class FPR_Contract extends Reduction_Contract {
constructor(end_condition=null) {
super();
Object.assign(this, { end_condition });
}
check_if_done(scanner, sequence) {
if (this.end_condition) {
return this.end_condition(scanner, sequence);
} else {
return super.check_if_done(scanner, sequence);
}
}
on_create_scanner(scanner) {
Object.assign(scanner, {
cycle_detected: false,
state_manager: new FPR_State(),
});
}
on_start_transform(scanner, sequence) {
super.on_start_transform(scanner, sequence);
const state_manager = scanner.state_manager;
state_manager.clear_journal();
const state = state_manager.gather_state(sequence); //Log initial state
state_manager.log_state(state);
scanner.cycle_detected = false;
}
on_reduction_made(scanner, sequence) {
const state_manager = scanner.state_manager;
const state = state_manager.gather_state(sequence);
const pre_count = state_manager.length;
state_manager.log_state(state);
const post_count = state_manager.length;
if (pre_count === post_count) {
scanner.cycle_detected = true;
}
}
assert_readiness(scanner, sequence) {
if (scanner.cycle_detected) {
throw new Error('Cycle detected');
}
}
}

View File

@@ -0,0 +1,103 @@
import { Reduction_Contract, FPR_Contract } from './contracts.mjs';
import * as CF from '@efforting.tech/data/field-configuration-factories';
export const Reduction_Order = new CF.symbol_set({
RULE_MAJOR: 'For each rule, scan the full sequence',
POSITION_MAJOR: 'For each item in the sequence, try all rules',
}, 'Reduction ordering');
export const Reduction_Settings = new CF.Schema({
reduction_order: CF.symbol(Reduction_Order, 'RULE_MAJOR'),
reduction_contract: CF.instance(Reduction_Contract, 'The reduction contract defines the implementation of the reduction scanner'),
rules: CF.factory(() => []),
}, 'Reduction settings');
//TODO: Consider whether we can implement some hierarchical sub schema, like classes - we could of course reuse things by defining and object and spread it in here
export const FP_Reduction_Settings = new CF.Schema({
reduction_order: CF.symbol(Reduction_Order, 'RULE_MAJOR'),
reduction_contract: CF.instance(FPR_Contract, 'The reduction contract defines the implementation of the reduction scanner'),
rules: CF.factory(() => []),
}, 'Fixed point reduction settings');
export class Reduction_Scanner {
static settings_schema = Reduction_Settings;
constructor(settings, use_lifecycle_cb=true) {
settings = this.constructor.settings_schema.load(settings);
Object.assign(this, { settings });
this.clear_transform_state();
if (use_lifecycle_cb) {
settings.reduction_contract.on_create_scanner(this);
}
}
perform_reduction(sequence) {
const { settings } = this;
switch (settings.reduction_order) {
case Reduction_Order.symbols.RULE_MAJOR:
for (const rule of settings.rules) {
for (let start_index=0; start_index < sequence.length; start_index++) {
const match = rule.match(sequence, start_index);
if (match) {
rule.action(this, sequence, match);
return true;
}
}
}
return false;
case Reduction_Order.symbols.POSITION_MAJOR:
for (let start_index=0; start_index < sequence.length; start_index++) {
for (const rule of settings.rules) {
const match = rule.match(sequence, start_index);
if (match) {
rule.action(this, sequence, match);
return true;
}
}
}
return false;
default:
throw new Error(`Unknown reduction order: ${this.reduction_order}`); //TODO: Force invalid configuration error
}
}
clear_transform_state() {
this.last_reduction_status = null;
this.reductions_attempted = 0;
this.reductions_made = 0;
}
transform(sequence) {
const { settings } = this;
const reduction_contract = settings.reduction_contract;
this.clear_transform_state();
reduction_contract.on_start_transform(this, sequence);
while (!reduction_contract.check_if_done(this, sequence)) {
reduction_contract.assert_readiness(this, sequence);
this.last_reduction_status = this.perform_reduction(sequence);
this.reductions_attempted++;
if (this.last_reduction_status) {
this.reductions_made++;
reduction_contract.on_reduction_made(this, sequence);
}
reduction_contract.on_reduction_attempted(this, sequence);
}
reduction_contract.on_end_transform(this, sequence);
return sequence;
}
}

View File

@@ -2,7 +2,7 @@ import { Item_Unresolvable } from '@efforting.tech/errors';
export class Abstract_Resolver {
resolve(item, extra_info={}) {
const result = this.resolve_handler(item);
const result = this.resolve_handler(item, extra_info);
if (!result?.handler) {
throw new Item_Unresolvable({ resolver: this, item });
}
@@ -17,10 +17,10 @@ export class Chained_Resolver extends Abstract_Resolver {
Object.assign(this, { chain_links });
}
resolve_handler(item) {
resolve_handler(item, extra_info={}) {
const { chain_links } = this;
for (const link of chain_links) {
const result = link.resolve_handler(item);
const result = link.resolve_handler(item, extra_info);
if (result?.handler) {
return result;
}
@@ -35,10 +35,10 @@ export class Predicate_Resolver extends Abstract_Resolver {
Object.assign(this, { rules });
}
resolve_handler(item) {
resolve_handler(item, extra_info={}) {
const { rules } = this;
for (const [predicate, handler] of rules) {
const predicate_result = predicate(item);
const predicate_result = predicate(item, extra_info);
// NOTE: to return a falsy predicate_result as a positive hit you must wrap it in something
if (predicate_result) {
return { handler, predicate_result };
@@ -52,7 +52,7 @@ export class RegExp_Resolver extends Predicate_Resolver {
// NOTE: Rules should be iterable as [predicate, handler] pairs
super();
Object.assign(this, {
rules: rules.map(([pattern, handler]) => [(str) => str.match(pattern), handler])
rules: rules.map(([pattern, handler]) => [(str, extra_info) => str.match(pattern), handler])
});
}
@@ -65,7 +65,7 @@ export class Mapping_Resolver extends Abstract_Resolver {
Object.assign(this, { rules, key_function });
}
resolve_handler(item) {
resolve_handler(item, extra_info={}) {
const { key_function, rules } = this;
const key = key_function ? key_function(item) : item;
return rules.get(key);
@@ -73,3 +73,26 @@ export class Mapping_Resolver extends Abstract_Resolver {
}
export class Sequential_Resolver extends Abstract_Resolver {
constructor(rules=[]) {
// NOTE: Rules should be iterable as [Abstract_Sequential_Condition, handler] pairs
super();
Object.assign(this, { rules });
}
resolve_handler(item, extra_info={}) {
const { rules } = this;
for (const [sequential_condition, handler] of rules) {
const sequential_condition_result = sequential_condition.match(item, extra_info);
// NOTE: to return a falsy predicate_result as a positive hit you must wrap it in something
if (sequential_condition_result) {
return { handler, sequential_condition_result };
}
}
}
}

View File

@@ -0,0 +1,134 @@
import * as CF from '@efforting.tech/data/field-configuration-factories';
export const Object_Serialization_Strategy = new CF.Schema({
identity: CF.boolean(true, 'Serialize based on identity'),
type: CF.boolean(true, 'Serialize based on constructor'),
symbols: CF.boolean(true, 'Include symbol-keyed properties'),
keys: CF.boolean(true, 'Include property keys'),
values: CF.boolean(true, 'Include property values'),
}, 'Object serialization strategy');
export const State_Serialization_Strategy = new CF.Schema({
//BUG - there is currently no way (I think) to put defaults into a sub schema - this should be fixed
array: CF.schema(Object_Serialization_Strategy, {
identity: false,
type: true,
symbols: false,
keys: false,
values: true,
}, 'State serialization strategy for Array'),
object: CF.schema(Object_Serialization_Strategy, {
identity: true,
type: true,
symbols: true,
keys: true,
values: true,
}, 'State serialization strategy for Object'),
}, 'State serialization strategy');
export class State_Serializer {
static settings_schema = State_Serialization_Strategy;
constructor(symbols=new Map(), strategy) {
strategy = this.constructor.settings_schema.load(strategy);
Object.assign(this, { symbols, strategy });
}
serialize_symbols(...symbols) {
const result = [];
for (const symbol of symbols) {
const existing = this.symbols.get(symbol);
if (existing !== undefined) {
result.push(existing);
} else {
const new_symbol_index = this.symbols.size;
this.symbols.set(symbol, new_symbol_index);
result.push(new_symbol_index);
}
}
return result;
}
serialize_object(object) {
const state_strategy = this.strategy;
const result = [];
switch (typeof(object)) {
case 'object':
if (object === null) {
return ['N'];
}
const is_array = object.constructor === Array;
const strategy = is_array ? state_strategy.array : state_strategy.object;
result.push(is_array ? 'a' : 'o');
if (strategy.identity) {
const existing_instance = this.symbols.get(object);
if (existing_instance !== undefined) {
return existing_instance;
}
const [instance] = this.serialize_symbols(object);
result.push(instance);
}
if (strategy.type) {
const [type] = this.serialize_symbols(object.constructor);
result.push(type);
}
if (strategy.symbols) {
const symbols = this.serialize_symbols(...Object.getOwnPropertySymbols(object));
result.push(symbols);
}
if (strategy.keys) {
const keys = Object.keys(object).map(item => this.serialize_object(item));
result.push(keys);
}
if (strategy.values) {
const values = Object.values(object).map(item => this.serialize_object(item));
result.push(values);
}
return result;
case 'string':
return ['s', ...this.serialize_symbols(typeof(object)), object];
case 'number':
return ['n', ...this.serialize_symbols(typeof(object)), object];
case 'boolean':
return [object ? 'T' : 'F'];
case 'function':
const [reference_type, reference_id] = this.serialize_symbols(typeof(object), object);
return ['f', reference_type, reference_id];
case 'symbol':
return ['S', ...this.serialize_symbols(object)];
case 'undefined':
return ['u'];
default:
throw new Error(typeof(object)); //TODO - proper error
}
}
};

View File

@@ -0,0 +1,47 @@
import { State_Serializer } from './state-serializer.mjs';
import { createHash } from 'node:crypto';
export class Abstract_State {
constructor(seen = new Set()) {
Object.assign(this, { seen });
}
gather_state(sequence) {
throw new Error(`gather_state(sequence) not implemented for ${this.constructor.name}`);
}
seen_state(state) {
return this.seen.has(state);
}
log_state(state) {
this.seen.add(state);
}
discard_state(state) {
this.seen.delete(state);
}
get length() {
return this.seen.size;
}
clear_journal() {
this.seen.clear();
}
}
export class FPR_State extends Abstract_State {
constructor(seen = new Set(), serializer = new State_Serializer()) {
super(seen);
Object.assign(this, { serializer });
}
gather_state(sequence) {
const str = JSON.stringify(this.serializer.serialize_object(sequence));
const hash = createHash('sha1').update(str).digest('hex');
return `${hash.slice(0, 8)}-${str}`;
}
}