jsonnet-microkanren

microKanren implementation in Jsonnet
git clone https://ccx.te2000.cz/git/jsonnet-microkanren
Log | Files | Refs

commit c019b4fe223ad8feb0f9078094b1d020c519d877
parent 0b1fa17fd5a927f6e96c15d62d8e4ab437b24c98
Author: Jan Pobříslo <ccx@te2000.cz>
Date:   Wed, 29 Nov 2023 08:10:09 +0000

WIP fixes

Diffstat:
A.gitignore | 1+
AMakefile | 2++
Aexample.jsonnet | 15+++++++++++++++
Mmicrokanren.libsonnet | 137++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Amicrokanren_checks.libsonnet | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 176 insertions(+), 57 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +*.sw[op] diff --git a/Makefile b/Makefile @@ -0,0 +1,2 @@ +example: + jsonnet -J . example.jsonnet diff --git a/example.jsonnet b/example.jsonnet @@ -0,0 +1,15 @@ +local uK = import 'microkanren.libsonnet'; +local uKc = import 'microkanren_checks.libsonnet'; + +{ + justFive: + local goal = uK.callFresh(function(q) q.eq(5)); + assert uKc.Goal(goal); + assert uKc.trace('goal', goal, true); + local stream = goal.pursue(uK.emptyState); + assert std.type(stream); + true, /* + assert uKc.Stream(stream); + stream.takeAll(), */ +} +// vim: sts=2 ts=2 sw=2 et diff --git a/microkanren.libsonnet b/microkanren.libsonnet @@ -1,28 +1,31 @@ +local uKc = import 'microkanren_checks.libsonnet'; +// helper functions + local baseStream(streamType) = { ['µK:stream']: streamType, - pull:: + pull():: if self['µK:stream'] == 'immature' then - self.call().pull + self.call().pull() else self, - takeAll:: + takeAll():: local takeRecursive(accumulator, stream) = - local mature = stream.pull, + local mature = stream.pull(); if mature['µK:stream'] == 'empty' then accumulator else - takeRecursive(accumulator + [mature.state], mature.next), + takeRecursive(accumulator + [mature.state], mature.next); takeRecursive([], self), take(n):: local takeRecursive(accumulator, stream) = if std.length(accumulator) >= n then accumulator else - local mature = stream.pull, + local mature = stream.pull(); if mature['µK:stream'] == 'empty' then accumulator else - takeRecursive(accumulator + [mature.state], mature.next), + takeRecursive(accumulator + [mature.state], mature.next); takeRecursive([], self), }; @@ -37,11 +40,14 @@ local immatureStream(func) = baseStream('immature') + { local makeGoal(callable) = { ['µK:goal']: callable, - pursue(state):: self['µK:goal'](state), + pursue(state):: + assert uKc.trace('pursuing goal in state', state, true); + assert uKc.State(state); + self['µK:goal'](state), }; local mplus(stream1, stream2) = - local t1 = stream1['µK:stream'], + local t1 = stream1['µK:stream']; if t1 == 'empty' then immatureStream(function() stream2) else if t1 == 'immature' then @@ -52,25 +58,26 @@ local mplus(stream1, stream2) = error 'Invalid stream'; local bind(stream, goal) = - local t = stream['µK:stream'], + local t = stream['µK:stream']; if t == 'empty' then stream else if t == 'immature' then - immatureStream(function() bind(stream1.call(), goal)) + immatureStream(function() bind(stream.call(), goal)) else if t == 'mature' then - immatureStream(function() mplus(goal.pursue(stream1.state), bind(stream2, goal))) + immatureStream(function() mplus(goal.pursue(stream.state), bind(stream.next, goal))) else error 'Invalid stream'; +// public interface { type(value): - local t = std.type(value), + local t = std.type(value); if t == 'object' then if std.type(std.get(value, 'µK:var')) == 'number' then 'variable' - else if std.type(std.get(value, 'µK:goal'), 'function') then + else if std.type(std.get(value, 'µK:goal')) == 'function' then 'goal' - else if std.type(std.get(value, 'µK:stream'), 'string') then + else if std.type(std.get(value, 'µK:stream')) == 'string' then 'stream' else t @@ -79,22 +86,22 @@ local bind(stream, goal) = // state emptyState: { - variable_count: 0, + variableCount: 0, substitution: { walk(var):: - if type(var) == 'variable' then - local bound = std.get(self, var['µK:var'])), + if $.type(var) == 'variable' then + local bound = std.get(self, var['µK:var']); if bound == null then - u + var else self.walk(bound) else var, unify(value1, value2):: - local w1 = self.walk(value1), - local w2 = self.walk(value1), - local t1 = type(w1), - local t2 = type(w2), + local w1 = self.walk(value1); + local w2 = self.walk(value1); + local t1 = $.type(w1); + local t2 = $.type(w2); if t1 == 'variable' && t2 == 'variable' && w1 == w2 then self else if t1 == 'variable' then @@ -106,7 +113,7 @@ local bind(stream, goal) = if std.length(w1) == 0 then self else - local s1 = self.unify(w1[0], w2[0]), + local s1 = self.unify(w1[0], w2[0]); if s1 == null then null else @@ -131,54 +138,70 @@ local bind(stream, goal) = else null, }, - freshVar:: { + freshVar():: { variable: { - ['µK:var']: self.variable_count, + ['µK:var']: self.variableCount, eq(value):: $.eq(self, value), }, - newState: self + {variable_count: self.variable_count + 1} + newState: self + {variableCount: self.variableCount + 1} }, }, // streams emptyStream: baseStream('empty'), - unitStream(state): matureStream(state, emptyStream), + unitStream(state): matureStream(state, $.emptyStream), // goal constructors eq(value1, value2): makeGoal(function(state) - local newState = state.unify(value1, value2), - if newState == null then - emptyStream + assert uKc.State(state); + local newSubst = state.substitution.unify(value1, value2); + if newSubst == null then + $.emptyStream else - unitStream(newState) - ), - conj(goal1, goal2): makeGoal(function(state) - $.bind(goal1.pursue(state), goal2) + $.unitStream(state + {substitution: newSubst}) ), - disj(goal1, goal2): makeGoal(function(state) - $.mplus(goal1.pursue(state), goal2.pursue(state)) - ), - callFresh(func): makeGoal(function(state) - local fresh = state.freshVar, - local newGoal = func(fresh.variable), - local t = type(newGoal), - (if t == 'goal' then - newGoal - else if t == 'array' then - local subgoals = std.reverse(newGoal[1::]), - (if newGoal[0] == 'and' then - std.foldl($.conj, subgoals[1::], subgoals[0]) - else if newGoal[0] == 'or' then - std.foldl($.disj, subgoals[1::], subgoals[0]) - else if newGoal[0] == 'eq' then - std.foldl($.disj, subgoals[1::], subgoals[0]) + conj(goal1, goal2): + assert uKc.Goal(goal1); + assert uKc.Goal(goal2); + makeGoal(function(state) + assert uKc.State(state); + $.bind(goal1.pursue(state), goal2) + ), + + disj(goal1, goal2): + assert uKc.Goal(goal1); + assert uKc.Goal(goal2); + makeGoal(function(state) + assert uKc.State(state); + $.mplus(goal1.pursue(state), goal2.pursue(state)) + ), + + callFresh(func): + assert std.assertEqual(std.type(func), 'function'); + makeGoal(function(state) + assert uKc.State(state); + local fresh = state.freshVar(); + assert uKc.Variable(fresh.variable); + assert uKc.State(fresh.newState); + local newGoal = func(fresh.variable); + local t = $.type(newGoal); + (if t == 'goal' then + newGoal + else if t == 'array' then + local subgoals = std.reverse(newGoal[1::]); + (if newGoal[0] == 'and' then + std.foldl($.conj, subgoals[1::], subgoals[0]) + else if newGoal[0] == 'or' then + std.foldl($.disj, subgoals[1::], subgoals[0]) + else if newGoal[0] == 'eq' then + std.foldl($.disj, subgoals[1::], subgoals[0]) + else + error 'Invalid goal' + ) else error 'Invalid goal' - ) - else - error 'Invalid goal' - ).pursue(fresh.newState) - ), + ).pursue(fresh.newState) + ), } // vim: sts=2 ts=2 sw=2 et diff --git a/microkanren_checks.libsonnet b/microkanren_checks.libsonnet @@ -0,0 +1,78 @@ +local safeString(value) = + local t = std.type(value); + if t == 'function' then + '<function>' + else if t == 'array' then + '[%s]' % [std.join(', ', std.map(safeString, value))] + else if t == 'object' then + '{%s}' % [std.join(', ', std.map( + function(f) '%s: %s' % [std.escapeStringJson(f.key), safeString(f.value)], + std.objectKeysValuesAll(value) + ))] + else + std.toString(value); + +local checkType(type) = function(value) std.assertEqual(std.type(value), type); +// value checks +{ + trace(str, val, rest): + std.trace("%s: %s" % [str, safeString(val)], rest), + + objectFields(obj, checks): + assert std.assertEqual(std.type(obj), 'object'); + assert std.assertEqual(std.objectFields(obj), std.objectFields(checks)); + std.all(std.map( + function(f) checks[f.key](f.value), + std.objectKeysValues(obj) + )), + + Variable(var): + $.objectFields(var, { + ['µK:var']: function(value) + assert std.assertEqual(std.type(value), 'number'); + assert value >= 0: "Incorrect variable number: %s" % [value]; + true, + }), + + VariableCount(vc): + assert vc >= 0: "Incorrect state.variableCount: less than zero"; + true, + + Substitution(subst): + assert std.assertEqual(std.type(subst), 'object'); + std.all(std.map( + function(f) std.assertEqual(std.type(f.name), 'number'), + std.objectKeysValues(subst) + )), + + State(state): + $.objectFields(state, { + variableCount: $.VariableCount, + substitution: $.Substitution, + }), + + Stream(stream): + assert std.assertEqual(std.type(stream), 'object'); + local t = stream['µK:stream']; + assert std.type(t) == 'string'; + if t == 'empty' then + $.objectFields(stream, {['µK:stream']: checkType('string')}) + else if t == 'immature' then + $.objectFields(stream, { + ['µK:stream']: checkType('string'), + call: checkType('function'), + }) + else if t == 'mature' then + $.objectFields(stream, { + ['µK:stream']: checkType('string'), + call: checkType('function'), + }) + else + error "Incorrect stream type", + + Goal(goal): $.objectFields(goal, { + ['µK:goal']: checkType('function'), + }), + +} +// vim: sts=2 ts=2 sw=2 et