From b49c3b87f3c65cd1bbdb7b14b4706336b5e6d5ad Mon Sep 17 00:00:00 2001 From: Associate 1 Date: Mon, 23 Feb 2026 12:26:17 -0700 Subject: [PATCH] Add PRI ALT and PRI PAR support (#76) Add PRI as a keyword and parse PRI ALT / PRI PAR constructs. Both are treated identically to their non-priority counterparts since Go's select and goroutines have no priority semantics. The AST records the Priority flag for potential future use. Note: This implementation accepts and correctly transpiles PRI ALT and PRI PAR, but does not provide true priority semantics. Go's select statement chooses uniformly at random among ready cases, and goroutines have no scheduling priority. Programs that depend on priority ordering for correctness will not behave as expected. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 7 ++-- TODO.md | 5 ++- ast/ast.go | 2 + codegen/e2e_concurrency_test.go | 39 +++++++++++++++++++ lexer/token.go | 3 ++ parser/parser.go | 20 ++++++++++ parser/parser_test.go | 66 +++++++++++++++++++++++++++++++++ 7 files changed, 137 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d1a5ae8..528a3ac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,8 +69,9 @@ Six packages, one pipeline: | `WHILE cond` | `for cond` | | `CASE x` | `switch x` | | `STOP` | `fmt.Fprintln(os.Stderr, ...)` + `select {}` | -| `ALT` | `select` | +| `ALT` / `PRI ALT` | `select` | | `ALT i = 0 FOR n` | `reflect.Select` with runtime case slice | +| `PRI PAR` | goroutines + `sync.WaitGroup` (same as `PAR`) | | `CHAN OF INT c:` | `c := make(chan int)` | | `c ! expr` | `c <- expr` | | `c ? x` | `x = <-c` | @@ -190,7 +191,7 @@ Typical workflow for a new language construct: ## What's Implemented -Preprocessor (`#IF`/`#ELSE`/`#ENDIF`/`#DEFINE`/`#INCLUDE` with search paths, include guards, include-once deduplication, `#COMMENT`/`#PRAGMA`/`#USE` ignored), module file generation from SConscript (`gen-module` subcommand), SEQ, PAR, IF, WHILE, CASE, ALT (with guards, timer timeouts, multi-statement bodies with scoped declarations, and replicators using `reflect.Select`), SKIP, STOP, variable/array/channel/timer declarations, abbreviations (`VAL INT x IS 42:`, `INT y IS z:`, `VAL []BYTE s IS "hi":`, untyped `VAL x IS expr:`), assignments (simple and indexed), channel send/receive, channel arrays (`[n]CHAN OF TYPE` with indexed send/receive, `[]CHAN OF TYPE` proc params, and multi-dimensional `[n][m]CHAN`/`[n][m]TYPE`/`[][]CHAN`/`[][]TYPE`), PROC (with VAL, RESULT, reference, CHAN, []CHAN, open array `[]TYPE`, fixed-size array `[n]TYPE`, and shared-type params), channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`, call-site annotations `out!`/`in?` accepted), multi-line parameter lists and expressions (lexer suppresses INDENT/DEDENT/NEWLINE inside parens/brackets and after continuation operators), FUNCTION (IS and VALOF forms with multi-statement bodies, including multi-result `INT, INT FUNCTION` with `RESULT a, b`), multi-assignment (`a, b := func(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`), KRoC-style colon terminators on PROC/FUNCTION (optional), INLINE function modifier (accepted and ignored), replicators on SEQ/PAR/IF/ALT (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `INT16 expr`, `INT32 expr`, `INT64 expr`, `BYTE expr`, `BOOL expr`, `REAL32 expr`, `REAL64 expr`, including BOOL↔numeric via `_boolToInt` helper and `!= 0` comparison, and ROUND/TRUNC qualifiers for float↔int conversions), INT16/INT32/INT64 types, REAL32/REAL64 types, hex integer literals (`#FF`, `#80000000`), string literals, byte literals (`'A'`, `'*n'` with occam escape sequences), built-in print procedures, protocols (simple, sequential, and variant), record types (with field access via bracket syntax), SIZE operator, array slices (`[arr FROM n FOR m]` and shorthand `[arr FOR m]` with slice assignment), array literals (`[1, 2, 3]`), nested PROCs/FUNCTIONs (local definitions as Go closures), MOSTNEG/MOSTPOS (type min/max constants for INT, INT16, INT32, INT64, BYTE, REAL32, REAL64), INITIAL declarations (`INITIAL INT x IS 42:` — mutable variable with initial value), checked (modular) arithmetic (`PLUS`, `MINUS`, `TIMES` — wrapping operators), RETYPES (bit-level type reinterpretation: `VAL INT X RETYPES X :` for float32→int, `VAL [2]INT X RETYPES X :` for float64→int pair), transputer intrinsics (LONGPROD, LONGDIV, LONGSUM, LONGDIFF, NORMALISE, SHIFTRIGHT, SHIFTLEFT — implemented as Go helper functions), CAUSEERROR (maps to `panic("CAUSEERROR")`). +Preprocessor (`#IF`/`#ELSE`/`#ENDIF`/`#DEFINE`/`#INCLUDE` with search paths, include guards, include-once deduplication, `#COMMENT`/`#PRAGMA`/`#USE` ignored), module file generation from SConscript (`gen-module` subcommand), SEQ, PAR, PRI PAR, IF, WHILE, CASE, ALT, PRI ALT (with guards, timer timeouts, multi-statement bodies with scoped declarations, and replicators using `reflect.Select`), SKIP, STOP, variable/array/channel/timer declarations, abbreviations (`VAL INT x IS 42:`, `INT y IS z:`, `VAL []BYTE s IS "hi":`, untyped `VAL x IS expr:`), assignments (simple and indexed), channel send/receive, channel arrays (`[n]CHAN OF TYPE` with indexed send/receive, `[]CHAN OF TYPE` proc params, and multi-dimensional `[n][m]CHAN`/`[n][m]TYPE`/`[][]CHAN`/`[][]TYPE`), PROC (with VAL, RESULT, reference, CHAN, []CHAN, open array `[]TYPE`, fixed-size array `[n]TYPE`, and shared-type params), channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`, call-site annotations `out!`/`in?` accepted), multi-line parameter lists and expressions (lexer suppresses INDENT/DEDENT/NEWLINE inside parens/brackets and after continuation operators), FUNCTION (IS and VALOF forms with multi-statement bodies, including multi-result `INT, INT FUNCTION` with `RESULT a, b`), multi-assignment (`a, b := func(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`), KRoC-style colon terminators on PROC/FUNCTION (optional), INLINE function modifier (accepted and ignored), replicators on SEQ/PAR/IF/ALT (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `INT16 expr`, `INT32 expr`, `INT64 expr`, `BYTE expr`, `BOOL expr`, `REAL32 expr`, `REAL64 expr`, including BOOL↔numeric via `_boolToInt` helper and `!= 0` comparison, and ROUND/TRUNC qualifiers for float↔int conversions), INT16/INT32/INT64 types, REAL32/REAL64 types, hex integer literals (`#FF`, `#80000000`), string literals, byte literals (`'A'`, `'*n'` with occam escape sequences), built-in print procedures, protocols (simple, sequential, and variant), record types (with field access via bracket syntax), SIZE operator, array slices (`[arr FROM n FOR m]` and shorthand `[arr FOR m]` with slice assignment), array literals (`[1, 2, 3]`), nested PROCs/FUNCTIONs (local definitions as Go closures), MOSTNEG/MOSTPOS (type min/max constants for INT, INT16, INT32, INT64, BYTE, REAL32, REAL64), INITIAL declarations (`INITIAL INT x IS 42:` — mutable variable with initial value), checked (modular) arithmetic (`PLUS`, `MINUS`, `TIMES` — wrapping operators), RETYPES (bit-level type reinterpretation: `VAL INT X RETYPES X :` for float32→int, `VAL [2]INT X RETYPES X :` for float64→int pair), transputer intrinsics (LONGPROD, LONGDIV, LONGSUM, LONGDIFF, NORMALISE, SHIFTRIGHT, SHIFTLEFT — implemented as Go helper functions), CAUSEERROR (maps to `panic("CAUSEERROR")`). ## Course Module Testing @@ -206,4 +207,4 @@ go vet /tmp/course_out.go ## Not Yet Implemented -PRI ALT/PRI PAR, PLACED PAR, PORT OF. See `TODO.md` for the full list with priorities. +PLACED PAR, PORT OF. See `TODO.md` for the full list with priorities. diff --git a/TODO.md b/TODO.md index 7290161..9605b87 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,8 @@ - **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process) - **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies - **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies -- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, multi-statement bodies, and replicators (`ALT i = 0 FOR n` using `reflect.Select`) +- **ALT / PRI ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, multi-statement bodies, and replicators (`ALT i = 0 FOR n` using `reflect.Select`). PRI ALT treated identically (Go has no priority select). +- **PRI PAR** — Priority parallel, treated identically to PAR (Go goroutines have no priority) - **SKIP** — No-op process - **STOP** — Error + deadlock @@ -94,7 +95,7 @@ | Feature | Notes | |---------|-------| -| **PRI ALT / PRI PAR** | Priority variants of ALT and PAR. | +| ~~**PRI ALT / PRI PAR**~~ | ~~Priority variants of ALT and PAR.~~ **Implemented** — treated as ALT/PAR (Go has no priority select). | | **PLACED PAR** | Assigning processes to specific hardware. | | **PORT OF** | Hardware port mapping. | | **`VAL []BYTE` abbreviations** | `VAL []BYTE cmap IS "0123456789ABCDEF":` — named string constants. | diff --git a/ast/ast.go b/ast/ast.go index 678035d..95752e6 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -100,6 +100,7 @@ type ParBlock struct { Token lexer.Token // the PAR token Statements []Statement Replicator *Replicator // optional replicator + Priority bool // true for PRI PAR } func (p *ParBlock) statementNode() {} @@ -387,6 +388,7 @@ type AltBlock struct { Token lexer.Token // the ALT token Cases []AltCase Replicator *Replicator // optional replicator + Priority bool // true for PRI ALT } func (a *AltBlock) statementNode() {} diff --git a/codegen/e2e_concurrency_test.go b/codegen/e2e_concurrency_test.go index af86288..229120a 100644 --- a/codegen/e2e_concurrency_test.go +++ b/codegen/e2e_concurrency_test.go @@ -353,3 +353,42 @@ func TestE2E_ReplicatedAltByte(t *testing.T) { t.Errorf("expected %q, got %q", expected, output) } } + +func TestE2E_PriAlt(t *testing.T) { + // Test PRI ALT: behaves the same as ALT in Go (no priority semantics) + occam := `SEQ + CHAN OF INT c1: + CHAN OF INT c2: + INT result: + PAR + c1 ! 42 + PRI ALT + c1 ? result + print.int(result) + c2 ? result + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_PriPar(t *testing.T) { + // Test PRI PAR: behaves the same as PAR in Go (no priority semantics) + occam := `SEQ + CHAN OF INT c: + INT result: + PRI PAR + c ! 99 + SEQ + c ? result + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "99\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} diff --git a/lexer/token.go b/lexer/token.go index 65a9c39..f514684 100644 --- a/lexer/token.go +++ b/lexer/token.go @@ -100,6 +100,7 @@ const ( TIMES // TIMES (modular multiplication keyword) ROUND_KW // ROUND (type conversion qualifier) TRUNC_KW // TRUNC (type conversion qualifier) + PRI // PRI (priority modifier for ALT/PAR) keyword_end ) @@ -195,6 +196,7 @@ var tokenNames = map[TokenType]string{ TIMES: "TIMES", ROUND_KW: "ROUND", TRUNC_KW: "TRUNC", + PRI: "PRI", } var keywords = map[string]TokenType{ @@ -248,6 +250,7 @@ var keywords = map[string]TokenType{ "TIMES": TIMES, "ROUND": ROUND_KW, "TRUNC": TRUNC_KW, + "PRI": PRI, } func (t TokenType) String() string { diff --git a/parser/parser.go b/parser/parser.go index 3bbf4a0..3f78b71 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -187,6 +187,8 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseParBlock() case lexer.ALT: return p.parseAltBlock() + case lexer.PRI: + return p.parsePriBlock() case lexer.SKIP: return &ast.Skip{Token: p.curToken} case lexer.STOP: @@ -1603,6 +1605,24 @@ func (p *Parser) parseReplicator() *ast.Replicator { return rep } +func (p *Parser) parsePriBlock() ast.Statement { + // curToken is PRI, expect ALT or PAR next + if p.peekTokenIs(lexer.ALT) { + p.nextToken() // consume ALT + block := p.parseAltBlock() + block.Priority = true + return block + } + if p.peekTokenIs(lexer.PAR) { + p.nextToken() // consume PAR + block := p.parseParBlock() + block.Priority = true + return block + } + p.addError(fmt.Sprintf("expected ALT or PAR after PRI, got %s", p.peekToken.Type)) + return nil +} + func (p *Parser) parseAltBlock() *ast.AltBlock { block := &ast.AltBlock{Token: p.curToken} diff --git a/parser/parser_test.go b/parser/parser_test.go index d4b8bb1..ad499ac 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -381,6 +381,72 @@ func TestAltBlockWithGuardedSkip(t *testing.T) { } } +func TestPriAltBlock(t *testing.T) { + input := `PRI ALT + c1 ? x + SKIP + c2 ? y + SKIP +` + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(program.Statements)) + } + + alt, ok := program.Statements[0].(*ast.AltBlock) + if !ok { + t.Fatalf("expected AltBlock, got %T", program.Statements[0]) + } + + if !alt.Priority { + t.Error("expected Priority to be true for PRI ALT") + } + + if len(alt.Cases) != 2 { + t.Fatalf("expected 2 cases, got %d", len(alt.Cases)) + } + + if alt.Cases[0].Channel != "c1" { + t.Errorf("expected channel 'c1', got %s", alt.Cases[0].Channel) + } + + if alt.Cases[1].Channel != "c2" { + t.Errorf("expected channel 'c2', got %s", alt.Cases[1].Channel) + } +} + +func TestPriParBlock(t *testing.T) { + input := `PRI PAR + x := 1 + y := 2 +` + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("expected 1 statement, got %d", len(program.Statements)) + } + + par, ok := program.Statements[0].(*ast.ParBlock) + if !ok { + t.Fatalf("expected ParBlock, got %T", program.Statements[0]) + } + + if !par.Priority { + t.Error("expected Priority to be true for PRI PAR") + } + + if len(par.Statements) != 2 { + t.Fatalf("expected 2 statements, got %d", len(par.Statements)) + } +} + func TestWhileLoop(t *testing.T) { input := `WHILE x > 0 x := x - 1