diff --git a/actr/buffer/buffer.go b/actr/buffer/buffer.go index 9479f47..66b4769 100644 --- a/actr/buffer/buffer.go +++ b/actr/buffer/buffer.go @@ -2,24 +2,17 @@ package buffer import ( - "fmt" - "sort" "strings" "golang.org/x/exp/slices" ) -// validBufferStates is a list of the valid buffer states to use with the _status chunk +// validStates is a list of valid buffer states to use when matching // TODO: needs review and correction // See: https://github.com/asmaloney/gactar/discussions/221 -var validBufferStates = map[string]bool{ - // buffer - "empty": true, - "full": true, - - // state - "busy": true, - "error": true, +var validStates = []string{ + "empty", + "full", } type Buffer struct { @@ -98,19 +91,12 @@ func (b List) Lookup(name string) Interface { return nil } -// IsValidBufferState checks if 'state' is a valid buffer state. -func IsValidBufferState(state string) bool { - v, ok := validBufferStates[state] - return v && ok +// IsValidState checks if 'state' is a valid buffer state. +func IsValidState(state string) bool { + return slices.Contains(validStates, state) } -// ValidBufferStatesStr returns a list of (sorted) valid buffer states. Used for error output. -func ValidBufferStatesStr() string { - keys := make([]string, 0, len(validBufferStates)) - for k := range validBufferStates { - keys = append(keys, fmt.Sprintf("'%s'", k)) - } - - sort.Strings(keys) - return strings.Join(keys, ", ") +// ValidStatesStr returns a list of (sorted) valid buffer states. Used for error output. +func ValidStatesStr() string { + return strings.Join(validStates, ", ") } diff --git a/actr/modules/modules.go b/actr/modules/modules.go index 1dc9932..cd4b134 100644 --- a/actr/modules/modules.go +++ b/actr/modules/modules.go @@ -2,6 +2,8 @@ package modules import ( + "strings" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" @@ -11,6 +13,11 @@ import ( const BuiltIn = "built-in" +var validStates = []string{ + "busy", + "error", +} + // Module is an ACT-R module type Module struct { Name string @@ -153,3 +160,13 @@ func FindModule(name string) ModuleInterface { return nil } + +// IsValidState checks if 'state' is a valid module state. +func IsValidState(state string) bool { + return slices.Contains(validStates, state) +} + +// ValidStatesStr returns a list of valid module states. Used for error output. +func ValidStatesStr() string { + return strings.Join(validStates, ", ") +} diff --git a/actr/production.go b/actr/production.go index d392764..c3ce3f5 100644 --- a/actr/production.go +++ b/actr/production.go @@ -58,8 +58,11 @@ func (c Constraint) String() string { } type Match struct { - Buffer buffer.Interface - Pattern *Pattern + Buffer buffer.Interface + + // matches either a pattern or a buffer status + Pattern *Pattern + BufferStatus *string } type Statement struct { diff --git a/amod/amod.go b/amod/amod.go index 1087b99..d5d5ec8 100644 --- a/amod/amod.go +++ b/amod/amod.go @@ -423,12 +423,24 @@ func addProductions(model *actr.Model, log *issueLog, productions *productionSec } for _, match := range production.Match.Items { - pattern, err := createChunkPattern(model, log, match.Pattern) + // check for buffer status match first + if match.BufferStatus != nil { + name := match.BufferStatus.Name + actrMatch := actr.Match{ + Buffer: model.LookupBuffer(name), + BufferStatus: &match.BufferStatus.Status, + } + + prod.Matches = append(prod.Matches, &actrMatch) + continue + } + + pattern, err := createChunkPattern(model, log, match.Chunk.Pattern) if err != nil { continue } - name := match.Name + name := match.Chunk.Name actrMatch := actr.Match{ Buffer: model.LookupBuffer(name), Pattern: pattern, @@ -454,8 +466,8 @@ func addProductions(model *actr.Model, log *issueLog, productions *productionSec } } - if match.When != nil { - for _, expr := range *match.When.Expressions { + if match.Chunk.When != nil { + for _, expr := range *match.Chunk.When.Expressions { comparison := actr.Equal if expr.Comparison.NotEqual != nil { diff --git a/amod/amod_productions_test.go b/amod/amod_productions_test.go index 1785bbf..375a5db 100644 --- a/amod/amod_productions_test.go +++ b/amod/amod_productions_test.go @@ -663,7 +663,7 @@ func Example_productionPrintStatement3() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { goal is empty } do { print } }`) @@ -678,7 +678,7 @@ func Example_productionPrintStatementInvalidID() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { retrieval is empty } do { print fooID } }`) @@ -694,7 +694,7 @@ func Example_productionPrintStatementInvalidNil() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { goal is empty } do { print nil } }`) @@ -710,7 +710,7 @@ func Example_productionPrintStatementInvalidVar() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { retrieval is empty } do { print ?fooVar } }`) @@ -726,7 +726,7 @@ func Example_productionPrintStatementWildcard() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { goal is empty } do { print * } }`) @@ -734,7 +734,7 @@ func Example_productionPrintStatementWildcard() { // ERROR: unexpected token "*" (expected "}") (line 9, col 13) } -func Example_productionMatchInternal() { +func Example_productionMatchBufferStatus() { generateToStdout(` ~~ model ~~ name: Test @@ -742,14 +742,14 @@ func Example_productionMatchInternal() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: error] } + match { retrieval is empty } do { print 42 } }`) // Output: } -func Example_productionMatchInternalSlots() { +func Example_productionMatchBufferStatusInvalidBuffer() { generateToStdout(` ~~ model ~~ name: Test @@ -757,15 +757,15 @@ func Example_productionMatchInternalSlots() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: busy error] } + match { foo is empty } do { print 42 } }`) // Output: - // ERROR: invalid chunk - '_status' expects 1 slot (line 8, col 20) + // ERROR: buffer 'foo' not found in production 'start' (line 8, col 10) } -func Example_productionMatchInternalInvalidStatus1() { +func Example_productionMatchBufferStatusInvalidStatus() { generateToStdout(` ~~ model ~~ name: Test @@ -773,15 +773,15 @@ func Example_productionMatchInternalInvalidStatus1() { ~~ init ~~ ~~ productions ~~ start { - match { goal [_status: something] } + match { retrieval is foo } do { print 42 } }`) // Output: - // ERROR: invalid _status 'something' for 'goal' in production 'start' (should be 'busy', 'empty', 'error', 'full') (line 8, col 25) + // ERROR: invalid status 'foo' for buffer 'retrieval' in production 'start' (should be one of: empty, full) (line 8, col 10) } -func Example_productionMatchInternalInvalidStatus2() { +func Example_productionMatchBufferStatusInvalidString() { generateToStdout(` ~~ model ~~ name: Test @@ -789,15 +789,46 @@ func Example_productionMatchInternalInvalidStatus2() { ~~ init ~~ ~~ productions ~~ start { - match { retrieval [_status: something] } + match { goal is 'empty' } + do { print 42 } + }`) + + // Output: + // ERROR: unexpected token "empty" (expected ) (line 8, col 18) +} + +func Example_productionMatchBufferStatusInvalidNumber() { + generateToStdout(` + ~~ model ~~ + name: Test + ~~ config ~~ + ~~ init ~~ + ~~ productions ~~ + start { + match { goal is 42 } + do { print 42 } + }`) + + // Output: + // ERROR: unexpected token "42" (expected ) (line 8, col 18) +} + +func Example_productionMatchModuleStatus() { + generateToStdout(` + ~~ model ~~ + name: Test + ~~ config ~~ + ~~ init ~~ + ~~ productions ~~ + start { + match { retrieval [_status: error] } do { print 42 } }`) // Output: - // ERROR: invalid _status 'something' for 'retrieval' in production 'start' (should be 'busy', 'empty', 'error', 'full') (line 8, col 30) } -func Example_productionMatchInternalInvalidStatusString() { +func Example_productionMatchModuleStatusInvalidStatus1() { generateToStdout(` ~~ model ~~ name: Test @@ -805,15 +836,15 @@ func Example_productionMatchInternalInvalidStatusString() { ~~ init ~~ ~~ productions ~~ start { - match { goal [_status: 'something'] } + match { retrieval [_status: 'foo'] } do { print 42 } }`) // Output: - // ERROR: invalid _status for 'goal' in production 'start' (should be 'busy', 'empty', 'error', 'full') (line 8, col 25) + // ERROR: invalid _status for 'retrieval' in production 'start' (should be one of: busy, error) (line 8, col 30) } -func Example_productionMatchInternalInvalidStatusNumber() { +func Example_productionMatchModuleStatusInvalidStatus2() { generateToStdout(` ~~ model ~~ name: Test @@ -821,10 +852,10 @@ func Example_productionMatchInternalInvalidStatusNumber() { ~~ init ~~ ~~ productions ~~ start { - match { goal [_status: 42] } + match { retrieval [ _status: bar ] } do { print 42 } }`) // Output: - // ERROR: invalid _status for 'goal' in production 'start' (should be 'busy', 'empty', 'error', 'full') (line 8, col 25) + // ERROR: invalid _status 'bar' for 'retrieval' in production 'start' (should be one of: busy, error) (line 8, col 31) } diff --git a/amod/lex.go b/amod/lex.go index 0e08d5b..8ddd740 100644 --- a/amod/lex.go +++ b/amod/lex.go @@ -131,6 +131,7 @@ var keywords []string = []string{ "do", "examples", "gactar", + "is", "match", "modules", "name", diff --git a/amod/parse.go b/amod/parse.go index 1ba9290..dddd157 100644 --- a/amod/parse.go +++ b/amod/parse.go @@ -221,7 +221,7 @@ type whenClause struct { Tokens []lexer.Token } -type matchItem struct { +type matchChunkItem struct { Name string `parser:"@Ident"` Pattern *pattern `parser:"@@"` When *whenClause `parser:"@@?"` @@ -229,6 +229,20 @@ type matchItem struct { Tokens []lexer.Token } +type matchBufferStatusItem struct { + Name string `parser:"@Ident"` + Status string `parser:"'is' @Ident"` + + Tokens []lexer.Token +} + +type matchItem struct { + Chunk *matchChunkItem `parser:"( @@"` + BufferStatus *matchBufferStatusItem `parser:"| @@)"` + + Tokens []lexer.Token +} + type match struct { Items []*matchItem `parser:"'match' '{' @@+ '}'"` diff --git a/amod/validate.go b/amod/validate.go index df2138b..1f209aa 100644 --- a/amod/validate.go +++ b/amod/validate.go @@ -4,6 +4,7 @@ import ( "github.com/alecthomas/participle/v2/lexer" "github.com/asmaloney/gactar/actr" "github.com/asmaloney/gactar/actr/buffer" + "github.com/asmaloney/gactar/actr/modules" "github.com/asmaloney/gactar/util/issues" ) @@ -118,65 +119,94 @@ func validatePattern(model *actr.Model, log *issueLog, pattern *pattern) (err er return } -// validateMatch verifies several aspects of a match item. -func validateMatch(match *match, model *actr.Model, log *issueLog, production *actr.Production) (err error) { - if match == nil { +// validateChunkMatch verifies several aspects of a chunk match item. +func validateChunkMatch(item *matchChunkItem, model *actr.Model, log *issueLog, production *actr.Production) (err error) { + name := item.Name + + bufferInterface := model.LookupBuffer(name) + if bufferInterface == nil { + log.errorTR(item.Tokens, 0, 1, "buffer '%s' not found in production '%s'", name, production.Name) + err = ErrCompile return } - for _, item := range match.Items { - name := item.Name + pattern := item.Pattern + pattern_err := validatePattern(model, log, pattern) + if pattern_err != nil { + err = ErrCompile + } - bufferInterface := model.LookupBuffer(name) - if bufferInterface == nil { - log.errorTR(item.Tokens, 0, 1, "buffer '%s' not found in production '%s'", name, production.Name) + // check _status chunks to ensure they have one of the allowed tests + if pattern.ChunkName == "_status" { + slot := *pattern.Slots[0] + slotItem := slot.ID + if slotItem == nil { + log.errorT(slot.Tokens, + "invalid _status for '%s' in production '%s' (should be one of: %v)", + name, production.Name, modules.ValidStatesStr()) err = ErrCompile - continue - } - - pattern := item.Pattern - pattern_err := validatePattern(model, log, pattern) - if pattern_err != nil { + } else if !modules.IsValidState(*slotItem) { + log.errorT(slot.Tokens, + "invalid _status '%s' for '%s' in production '%s' (should be one of: %v)", + *slotItem, name, production.Name, modules.ValidStatesStr()) err = ErrCompile } + } - // check _status chunks to ensure they have one of the allowed tests - if pattern.ChunkName == "_status" { - slot := *pattern.Slots[0] - slotItem := slot.ID - - if slotItem == nil { - log.errorT(slot.Tokens, - "invalid _status for '%s' in production '%s' (should be %v)", - name, production.Name, buffer.ValidBufferStatesStr()) - err = ErrCompile + // If we have constraints, check them + if item.When != nil { + for _, expr := range *item.When.Expressions { + // Check that we haven't negated it in the pattern and then tried to constrain it further + for _, slot := range pattern.Slots { + if slot.Not && slot.Var != nil { + if expr.LHS == *slot.Var { + log.errorTR(expr.Tokens, 1, 2, "cannot further constrain a negated variable '%s'", expr.LHS) + break + } + } + } - } else if !buffer.IsValidBufferState(*slotItem) { - log.errorT(slot.Tokens, - "invalid _status '%s' for '%s' in production '%s' (should be %v)", - *slotItem, name, production.Name, buffer.ValidBufferStatesStr()) - err = ErrCompile + // Check that we aren't comparing to ourselves + if expr.RHS.Var != nil && expr.LHS == *expr.RHS.Var { + log.errorT(expr.RHS.Tokens, "cannot compare a variable to itself '%s'", expr.LHS) } } + } - // If we have constraints, check them - if item.When != nil { - for _, expr := range *item.When.Expressions { - // Check that we haven't negated it in the pattern and then tried to constrain it further - for _, slot := range pattern.Slots { - if slot.Not && slot.Var != nil { - if expr.LHS == *slot.Var { - log.errorTR(expr.Tokens, 1, 2, "cannot further constrain a negated variable '%s'", expr.LHS) - break - } - } - } + return +} - // Check that we aren't comparing to ourselves - if expr.RHS.Var != nil && expr.LHS == *expr.RHS.Var { - log.errorT(expr.RHS.Tokens, "cannot compare a variable to itself '%s'", expr.LHS) - } - } +// validateBufferStatusMatch verifies several aspects of a buffer status match item. +func validateBufferStatusMatch(item *matchBufferStatusItem, model *actr.Model, log *issueLog, production *actr.Production) (err error) { + name := item.Name + + bufferInterface := model.LookupBuffer(name) + if bufferInterface == nil { + log.errorTR(item.Tokens, 0, 1, "buffer '%s' not found in production '%s'", name, production.Name) + err = ErrCompile + } + + if !buffer.IsValidState(item.Status) { + log.errorT(item.Tokens, + "invalid status '%s' for buffer '%s' in production '%s' (should be one of: %v)", + item.Status, name, production.Name, buffer.ValidStatesStr()) + err = ErrCompile + } + + return +} + +// validateMatch verifies several aspects of a match item. +func validateMatch(match *match, model *actr.Model, log *issueLog, production *actr.Production) (err error) { + if match == nil { + return + } + + for _, item := range match.Items { + if item.Chunk != nil { + err = validateChunkMatch(item.Chunk, model, log, production) + } else if item.BufferStatus != nil { + err = validateBufferStatusMatch(item.BufferStatus, model, log, production) } } @@ -394,10 +424,15 @@ func validateVariableUsage(log *issueLog, match *match, do *do) { // Walk the matches and store var ref counts for _, match := range match.Items { - addPatternRefs(match.Pattern, true) + // only need to consider chunk matchers + if match.Chunk == nil { + continue + } + + addPatternRefs(match.Chunk.Pattern, true) - if match.When != nil { - when := match.When + if match.Chunk.When != nil { + when := match.Chunk.When if when.Expressions != nil { for _, expr := range *when.Expressions { diff --git a/framework/ccm_pyactr/ccm_pyactr.go b/framework/ccm_pyactr/ccm_pyactr.go index 31b3e0c..0c8eb95 100644 --- a/framework/ccm_pyactr/ccm_pyactr.go +++ b/framework/ccm_pyactr/ccm_pyactr.go @@ -471,22 +471,15 @@ func (c CCMPyACTR) outputPattern(pattern *actr.Pattern) { } func (c CCMPyACTR) outputMatch(match *actr.Match) { - var name string - if match.Buffer != nil { - name = match.Buffer.BufferName() - } + bufferName := match.Buffer.BufferName() - chunkTypeName := match.Pattern.Chunk.TypeName - if actr.IsInternalChunkType(chunkTypeName) { - if chunkTypeName == "_status" { - status := match.Pattern.Slots[0] - if name == "retrieval" { - name = "memory" - } - c.Write("%s='%s:True'", name, status) + if match.BufferStatus != nil { + if bufferName == "retrieval" { + bufferName = "memory" } + c.Write("%s='%s:True'", bufferName, *match.BufferStatus) } else { - c.Write("%s=", name) + c.Write("%s=", bufferName) c.outputPattern(match.Pattern) } } diff --git a/framework/pyactr/pyactr.go b/framework/pyactr/pyactr.go index 3c0c00e..cca107f 100644 --- a/framework/pyactr/pyactr.go +++ b/framework/pyactr/pyactr.go @@ -476,23 +476,23 @@ func (p PyACTR) outputPattern(pattern *actr.Pattern, tabs int) { func (p PyACTR) outputMatch(match *actr.Match) { bufferName := match.Buffer.BufferName() - chunkName := match.Pattern.Chunk.TypeName - if actr.IsInternalChunkType(chunkName) { - if chunkName == "_status" { - status := match.Pattern.Slots[0] - p.Writeln(" ?%s>", bufferName) + if match.BufferStatus != nil { + p.Writeln(" ?%s>", bufferName) + p.Writeln(" buffer %s", *match.BufferStatus) + } else { + chunkName := match.Pattern.Chunk.TypeName - // Table 2.1 page 24 of pyactr book - if status.String() == "full" || status.String() == "empty" { - p.Writeln(" buffer %s", status) - } else { + if actr.IsInternalChunkType(chunkName) { + if chunkName == "_status" { + status := match.Pattern.Slots[0] + p.Writeln(" ?%s>", bufferName) p.Writeln(" state %s", status) } + } else { + p.Writeln(" =%s>", bufferName) + p.outputPattern(match.Pattern, 2) } - } else { - p.Writeln(" =%s>", bufferName) - p.outputPattern(match.Pattern, 2) } } diff --git a/framework/vanilla_actr/vanilla_actr.go b/framework/vanilla_actr/vanilla_actr.go index e600efe..60337ac 100644 --- a/framework/vanilla_actr/vanilla_actr.go +++ b/framework/vanilla_actr/vanilla_actr.go @@ -474,22 +474,22 @@ func (v VanillaACTR) outputPattern(pattern *actr.Pattern, tabs int) { func (v VanillaACTR) outputMatch(match *actr.Match) { bufferName := match.Buffer.BufferName() - chunkTypeName := match.Pattern.Chunk.TypeName - - if actr.IsInternalChunkType(chunkTypeName) { - if chunkTypeName == "_status" { - status := match.Pattern.Slots[0] - v.Writeln("\t?%s>", bufferName) + if match.BufferStatus != nil { + v.Writeln("\t?%s>", bufferName) + v.Writeln("\t\tbuffer %s", *match.BufferStatus) + } else { + chunkTypeName := match.Pattern.Chunk.TypeName - if status.String() == "full" || status.String() == "empty" { - v.Writeln("\t\tbuffer %s", status) - } else { + if actr.IsInternalChunkType(chunkTypeName) { + if chunkTypeName == "_status" { + status := match.Pattern.Slots[0] + v.Writeln("\t?%s>", bufferName) v.Writeln("\t\tstate %s", status) } + } else { + v.Writeln("\t=%s>", bufferName) + v.outputPattern(match.Pattern, 2) } - } else { - v.Writeln("\t=%s>", bufferName) - v.outputPattern(match.Pattern, 2) } }