Claude Code transcript - page 1/2

Please implement a fix for issue #44.

Thinking

Let me first look at issue #44 to understand what needs to be fixed.

$ Bash
View GitHub issue #44
gh issue view 44
title:	Support INLINE function modifier
state:	OPEN
author:	associate-1
labels:	
comments:	1
assignees:	
projects:	
milestone:	
number:	44
--
## Description

The `INLINE` keyword modifier on function declarations is not recognized by the parser. This is a standard occam feature (not occam-pi).

### Syntax

```occam
INT INLINE FUNCTION seconds (VAL INT s)
  INT ticks:
  VALOF
    ticks := s * 1000000
    RESULT ticks
:
```

The `INLINE` keyword appears between the return type and `FUNCTION` keyword, indicating the function should be inlined at call sites.

### Affected files

- `kroc/modules/course/examples/bar.occ` (3 occurrences: lines 29, 45, 61)

### Current error

```
bar.occ:29: expected :, got FUNCTION
```

The parser sees `INT` as a type and then expects a `:` (variable declaration), but encounters `INLINE FUNCTION` which it doesn't understand.

### Proposed approach

For transpilation purposes, `INLINE` is purely an optimization hint and can be ignored — the function should be parsed and generated exactly as if `INLINE` were absent. The parser just needs to skip the `INLINE` keyword when it appears before `FUNCTION` in a declaration context.

### Related

- #43 - Course examples tracking issue
$ Bash
View comments on issue #44
gh issue view 44 --comments
author:	dboreham
association:	member
edited:	false
status:	none
--
Agreed, simply making the INLINE keyword a no-op seems reasonable. Definitely document somewhere that this is the case, so as to not confuse future users who do expect their functions to be inlined.
--
Thinking

Now let me understand the current codebase to implement this fix. I need to:

  1. Add an INLINE token to the lexer
  2. Skip it in the parser when it appears before FUNCTION

Let me look at the relevant files.

Let me explore the relevant code to understand the current implementation.

Grep
{
  "pattern": "INLINE",
  "output_mode": "content",
  "-n": true
}
No matches found
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/lexer/token.go"
}
     1→package lexer
     2→
     3→type TokenType int
     4→
     5→const (
     6→	// Special tokens
     7→	ILLEGAL TokenType = iota
     8→	EOF
     9→	NEWLINE
    10→	INDENT
    11→	DEDENT
    12→
    13→	// Literals
    14→	IDENT     // variable names, procedure names
    15→	INT       // integer literal
    16→	STRING    // string literal
    17→	BYTE_LIT  // byte literal: 'A', '*n', etc.
    18→
    19→	// Operators
    20→	ASSIGN   // :=
    21→	PLUS     // +
    22→	MINUS    // -
    23→	MULTIPLY // *
    24→	DIVIDE   // /
    25→	MODULO   // \ (backslash in Occam)
    26→	EQ       // =
    27→	NEQ      // <>
    28→	LT       // <
    29→	GT       // >
    30→	LE       // <=
    31→	GE       // >=
    32→	SEND      // !
    33→	RECEIVE   // ?
    34→	AMPERSAND // & (guard separator in ALT)
    35→	BITAND    // /\  (bitwise AND)
    36→	BITOR     // \/  (bitwise OR)
    37→	BITXOR    // ><  (bitwise XOR)
    38→	BITNOT    // ~   (bitwise NOT)
    39→	LSHIFT    // <<  (left shift)
    40→	RSHIFT    // >>  (right shift)
    41→
    42→	// Delimiters
    43→	LPAREN    // (
    44→	RPAREN    // )
    45→	LBRACKET  // [
    46→	RBRACKET  // ]
    47→	COMMA     // ,
    48→	COLON     // :
    49→	SEMICOLON // ;
    50→
    51→	// Keywords
    52→	keyword_beg
    53→	SEQ
    54→	PAR
    55→	ALT
    56→	IF
    57→	CASE
    58→	ELSE
    59→	WHILE
    60→	FOR
    61→	FROM
    62→	PROC
    63→	FUNC
    64→	FUNCTION
    65→	VALOF
    66→	RESULT
    67→	IS
    68→	CHAN
    69→	OF
    70→	TRUE
    71→	FALSE
    72→	NOT
    73→	AND
    74→	OR
    75→	SKIP
    76→	STOP
    77→	INT_TYPE
    78→	BYTE_TYPE
    79→	BOOL_TYPE
    80→	REAL_TYPE
    81→	REAL32_TYPE
    82→	REAL64_TYPE
    83→	TIMER
    84→	AFTER
    85→	VAL
    86→	PROTOCOL
    87→	RECORD
    88→	SIZE_KW
    89→	STEP
    90→	MOSTNEG_KW
    91→	MOSTPOS_KW
    92→	INITIAL
    93→	RETYPES  // RETYPES (bit-level type reinterpretation)
    94→	PLUS_KW  // PLUS (modular addition keyword, distinct from + symbol)
    95→	MINUS_KW // MINUS (modular subtraction keyword, distinct from - symbol)
    96→	TIMES    // TIMES (modular multiplication keyword)
    97→	keyword_end
    98→)
    99→
   100→var tokenNames = map[TokenType]string{
   101→	ILLEGAL: "ILLEGAL",
   102→	EOF:     "EOF",
   103→	NEWLINE: "NEWLINE",
   104→	INDENT:  "INDENT",
   105→	DEDENT:  "DEDENT",
   106→
   107→	IDENT:    "IDENT",
   108→	INT:      "INT",
   109→	STRING:   "STRING",
   110→	BYTE_LIT: "BYTE_LIT",
   111→
   112→	ASSIGN:   ":=",
   113→	PLUS:     "+",
   114→	MINUS:    "-",
   115→	MULTIPLY: "*",
   116→	DIVIDE:   "/",
   117→	MODULO:   "\\",
   118→	EQ:       "=",
   119→	NEQ:      "<>",
   120→	LT:       "<",
   121→	GT:       ">",
   122→	LE:       "<=",
   123→	GE:       ">=",
   124→	SEND:      "!",
   125→	RECEIVE:   "?",
   126→	AMPERSAND: "&",
   127→	BITAND:    "/\\",
   128→	BITOR:     "\\/",
   129→	BITXOR:    "><",
   130→	BITNOT:    "~",
   131→	LSHIFT:    "<<",
   132→	RSHIFT:    ">>",
   133→
   134→	LPAREN:    "(",
   135→	RPAREN:    ")",
   136→	LBRACKET:  "[",
   137→	RBRACKET:  "]",
   138→	COMMA:     ",",
   139→	COLON:     ":",
   140→	SEMICOLON: ";",
   141→
   142→	SEQ:       "SEQ",
   143→	PAR:       "PAR",
   144→	ALT:       "ALT",
   145→	IF:        "IF",
   146→	CASE:      "CASE",
   147→	ELSE:      "ELSE",
   148→	WHILE:     "WHILE",
   149→	FOR:       "FOR",
   150→	FROM:      "FROM",
   151→	PROC:      "PROC",
   152→	FUNC:      "FUNC",
   153→	FUNCTION:  "FUNCTION",
   154→	VALOF:     "VALOF",
   155→	RESULT:    "RESULT",
   156→	IS:        "IS",
   157→	CHAN:      "CHAN",
   158→	OF:        "OF",
   159→	TRUE:      "TRUE",
   160→	FALSE:     "FALSE",
   161→	NOT:       "NOT",
   162→	AND:       "AND",
   163→	OR:        "OR",
   164→	SKIP:      "SKIP",
   165→	STOP:      "STOP",
   166→	INT_TYPE:  "INT",
   167→	BYTE_TYPE: "BYTE",
   168→	BOOL_TYPE: "BOOL",
   169→	REAL_TYPE:   "REAL",
   170→	REAL32_TYPE: "REAL32",
   171→	REAL64_TYPE: "REAL64",
   172→	TIMER:       "TIMER",
   173→	AFTER:    "AFTER",
   174→	VAL:       "VAL",
   175→	PROTOCOL:  "PROTOCOL",
   176→	RECORD:    "RECORD",
   177→	SIZE_KW:    "SIZE",
   178→	STEP:       "STEP",
   179→	MOSTNEG_KW: "MOSTNEG",
   180→	MOSTPOS_KW: "MOSTPOS",
   181→	INITIAL:    "INITIAL",
   182→	RETYPES:    "RETYPES",
   183→	PLUS_KW:    "PLUS",
   184→	MINUS_KW:   "MINUS",
   185→	TIMES:      "TIMES",
   186→}
   187→
   188→var keywords = map[string]TokenType{
   189→	"SEQ":   SEQ,
   190→	"PAR":   PAR,
   191→	"ALT":   ALT,
   192→	"IF":    IF,
   193→	"CASE":  CASE,
   194→	"ELSE":  ELSE,
   195→	"WHILE": WHILE,
   196→	"FOR":   FOR,
   197→	"FROM":  FROM,
   198→	"PROC":  PROC,
   199→	"FUNC":     FUNC,
   200→	"FUNCTION": FUNCTION,
   201→	"VALOF":    VALOF,
   202→	"RESULT":   RESULT,
   203→	"IS":       IS,
   204→	"CHAN":     CHAN,
   205→	"OF":    OF,
   206→	"TRUE":  TRUE,
   207→	"FALSE": FALSE,
   208→	"NOT":   NOT,
   209→	"AND":   AND,
   210→	"OR":    OR,
   211→	"SKIP":  SKIP,
   212→	"STOP":  STOP,
   213→	"INT":   INT_TYPE,
   214→	"BYTE":  BYTE_TYPE,
   215→	"BOOL":  BOOL_TYPE,
   216→	"REAL":   REAL_TYPE,
   217→	"REAL32": REAL32_TYPE,
   218→	"REAL64": REAL64_TYPE,
   219→	"TIMER":  TIMER,
   220→	"AFTER": AFTER,
   221→	"VAL":      VAL,
   222→	"PROTOCOL": PROTOCOL,
   223→	"RECORD":   RECORD,
   224→	"SIZE":     SIZE_KW,
   225→	"STEP":     STEP,
   226→	"MOSTNEG":  MOSTNEG_KW,
   227→	"MOSTPOS":  MOSTPOS_KW,
   228→	"INITIAL":  INITIAL,
   229→	"RETYPES":  RETYPES,
   230→	"PLUS":     PLUS_KW,
   231→	"MINUS":    MINUS_KW,
   232→	"TIMES":    TIMES,
   233→}
   234→
   235→func (t TokenType) String() string {
   236→	if name, ok := tokenNames[t]; ok {
   237→		return name
   238→	}
   239→	return "UNKNOWN"
   240→}
   241→
   242→func LookupIdent(ident string) TokenType {
   243→	if tok, ok := keywords[ident]; ok {
   244→		return tok
   245→	}
   246→	return IDENT
   247→}
   248→
   249→type Token struct {
   250→	Type    TokenType
   251→	Literal string
   252→	Line    int
   253→	Column  int
   254→}
   255→
Grep
{
  "pattern": "FUNCTION",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true,
  "context": 3,
  "head_limit": 80
}
166-	case lexer.INITIAL:
167-		return p.parseInitialDecl()
168-	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
169:		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
170-			return p.parseFuncDecl()
171-		}
172-		return p.parseVarDeclOrAbbreviation()
--
2164-		ReturnTypes: []string{p.curToken.Literal},
2165-	}
2166-
2167:	// Parse additional return types for multi-result functions: INT, INT FUNCTION
2168-	for p.peekTokenIs(lexer.COMMA) {
2169-		p.nextToken() // consume comma
2170-		p.nextToken() // move to next type
2171-		fn.ReturnTypes = append(fn.ReturnTypes, p.curToken.Literal)
2172-	}
2173-
2174:	// Consume FUNCTION keyword
2175-	p.nextToken()
2176-
2177-	if !p.expectPeek(lexer.IDENT) {
--
2189-		return nil
2190-	}
2191-
2192:	// Force all params to IsVal = true (occam FUNCTION params are always VAL)
2193-	for i := range fn.Params {
2194-		fn.Params[i].IsVal = true
2195-	}
--
2200-	}
2201-
2202-	if !p.peekTokenIs(lexer.INDENT) {
2203:		p.addError("expected indented body after FUNCTION declaration")
2204-		return fn
2205-	}
2206-	funcLevel := p.indentLevel

[Showing results with pagination = limit: 80, offset: 0]
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/examples/bar.occ"
}
     1→---------------------------------------------------------------------------
     2→--
     3→--      bar.occ -- animated fair ALTing
     4→--      Copyright (C) 2000 Fred Barnes
     5→--	Some fixes/mods Oct 2001, Peter Welch
     6→--
     7→--      This program is free software; you can redistribute it and/or modify
     8→--      it under the terms of the GNU General Public License as published by
     9→--      the Free Software Foundation; either version 2 of the License, or
    10→--      (at your option) any later version.
    11→--
    12→--      This program is distributed in the hope that it will be useful,
    13→--      but WITHOUT ANY WARRANTY; without even the implied warranty of
    14→--      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    15→--      GNU General Public License for more details.
    16→--
    17→--      You should have received a copy of the GNU General Public License
    18→--      along with this program; if not, write to the Free Software
    19→--      Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
    20→--
    21→
    22→#INCLUDE "course.module"
    23→--#INCLUDE "time.module"
    24→
    25→--{{{  FUNCTION seconds
    26→--* Convert seconds to timer ticks.
    27→-- @param s Time in seconds
    28→-- @return Time in timer ticks
    29→INT INLINE FUNCTION seconds (VAL INT s)
    30→  INT ticks:
    31→  VALOF
    32→    #IF TARGET.BITS.PER.WORD = 16
    33→    ticks := s * 1000
    34→    #ELSE
    35→    ticks := s * 1000000
    36→    #ENDIF
    37→    RESULT ticks
    38→:
    39→--}}}
    40→
    41→--{{{  FUNCTION milliseconds
    42→--* Convert milliseconds to timer ticks.
    43→-- @param ms Time in milliseconds
    44→-- @return Time in timer ticks
    45→INT INLINE FUNCTION milliseconds (VAL INT ms)
    46→  INT ticks:
    47→  VALOF
    48→    #IF TARGET.BITS.PER.WORD = 16
    49→    ticks := ms
    50→    #ELSE
    51→    ticks := ms * 1000
    52→    #ENDIF
    53→    RESULT ticks
    54→:
    55→--}}}
    56→
    57→--{{{  FUNCTION microseconds
    58→--* Convert microseconds to timer ticks.
    59→-- @param us Time in microseconds
    60→-- @return Time in timer ticks
    61→INT INLINE FUNCTION microseconds (VAL INT us)
    62→  INT ticks:
    63→  VALOF
    64→    #IF TARGET.BITS.PER.WORD = 16
    65→    ticks := us / 1000
    66→    #ELSE
    67→    ticks := us
    68→    #ENDIF
    69→    RESULT ticks
    70→:
    71→--}}}
    72→
    73→--{{{  O.REQ protocol
    74→PROTOCOL O.REQ
    75→  CASE
    76→    movecursor; BYTE; BYTE
    77→    string; INT::[]BYTE
    78→    number; INT
    79→    thirsty; INT
    80→    happy; INT
    81→    sad; INT
    82→    blank
    83→    bar.blank
    84→    bar.happy
    85→    terminate
    86→:
    87→--}}}
    88→
    89→--{{{  constants
    90→
    91→VAL INT PRI.ALT IS 0:
    92→VAL INT FAIRISH.ALT IS 1:
    93→VAL INT FAIR.ALT IS 2:
    94→
    95→VAL INT num.punters IS 20:
    96→
    97→VAL INT drunk.pints IS 8:
    98→VAL INT limit.pints IS 16:
    99→
   100→VAL BYTE bar.left IS 1:
   101→VAL BYTE punter.left IS 14:
   102→VAL BYTE punter.top IS 2:
   103→VAL BYTE punter.right IS 30:
   104→VAL BYTE punter.bcount IS 35:
   105→VAL BYTE barkeep.x IS 6:
   106→VAL BYTE help.y IS ((BYTE num.punters) + punter.top) + 2:
   107→
   108→VAL []BYTE punter.blank IS "   ":
   109→VAL [][]BYTE punter.happy IS [":-)", "%-]", "X-|"]:
   110→VAL [][]BYTE punter.thirsty IS [":-O", "%-P", "X-|"]:
   111→VAL [][]BYTE punter.sad IS [":-(", "%-[", "X-|"]:
   112→VAL []BYTE barkeep.blank IS "    ":
   113→VAL []BYTE barkeep.happy IS "=:-]":
   114→
   115→--}}}
   116→
   117→--{{{  PROC draw.bar (SHARED CHAN O.REQ out!)
   118→PROC draw.bar (SHARED CHAN O.REQ out!)
   119→  VAL []BYTE bar.end IS "----------||":
   120→  VAL []BYTE bar.bit IS "          ||":
   121→  VAL []BYTE help IS "keys: 0 = normal (arbitrary) alt,  1 = fair(ish) alt,  2 = fair alt":
   122→  CLAIM out!
   123→    SEQ
   124→      out ! movecursor; 1; 1
   125→      out ! string; (SIZE bar.end)::bar.end
   126→      SEQ i = INT punter.top FOR INT num.punters
   127→        SEQ
   128→          out ! movecursor; 1; BYTE i
   129→          out ! string; (SIZE bar.bit)::bar.bit
   130→      out ! movecursor; 1; (BYTE num.punters) + punter.top
   131→      out ! string; (SIZE bar.end)::bar.end
   132→      out ! movecursor; 1; help.y
   133→      out ! string; (SIZE help)::help
   134→:
   135→--}}}
   136→
   137→--{{{  PROC punter (VAL INT me, seed,  CHAN BYTE request!, response?, SHARED CHAN O.REQ out!)
   138→PROC punter (VAL INT me, seed, CHAN INT request!, response?, SHARED CHAN O.REQ out!)
   139→  VAL INT speed.const IS milliseconds (20):
   140→
   141→  BYTE my.x, my.y:
   142→  INT num.pints:
   143→  VAL INT FINE IS 0:
   144→  VAL INT DRUNK IS 1:
   145→  VAL INT DEAD IS 2:
   146→  INITIAL INT state IS FINE:
   147→  INITIAL INT my.seed IS seed + me:
   148→  TIMER tim:
   149→  INT t:
   150→  SEQ
   151→    --{{{  Initialise
   152→    num.pints := 0
   153→    my.x := punter.right
   154→    my.y := (BYTE me) + punter.top
   155→    CLAIM out!
   156→      SEQ
   157→        out ! movecursor; punter.bcount; my.y
   158→        out ! number; num.pints
   159→    --}}}
   160→    --{{{  Shake up random number seed
   161→    SEQ i = 0 FOR 1000
   162→      t, my.seed := random (1<<6, my.seed)
   163→    --}}}
   164→    --{{{  Main loop
   165→    WHILE (state <> DEAD)
   166→      INT tdelay:
   167→      SEQ
   168→        --{{{  Think
   169→        SEQ
   170→          CLAIM out!
   171→            SEQ
   172→              out ! movecursor; my.x; my.y
   173→              out ! happy; state
   174→          tdelay, my.seed := random (seconds (2), my.seed)
   175→          tim ? t
   176→          tim ? AFTER (t PLUS tdelay)
   177→        --}}}
   178→        --{{{  Get thirsty and go to the bar
   179→        SEQ i = 0 FOR INT (punter.right - punter.left)
   180→          SEQ
   181→            CLAIM out!
   182→              SEQ
   183→                out ! movecursor; my.x; my.y
   184→                out ! blank
   185→                my.x := (my.x - 1)
   186→                out ! movecursor; my.x; my.y
   187→                out ! thirsty; state
   188→            tim ? t
   189→            tim ? AFTER (t PLUS speed.const)
   190→        --}}}
   191→        --{{{  Get unhappy and order drink
   192→        SEQ
   193→          CLAIM out!
   194→            SEQ
   195→              out ! movecursor; my.x; my.y
   196→              out ! sad; state
   197→          request ! me
   198→          INT any:
   199→          response ? any
   200→        --}}}
   201→        --{{{  Update beer count and move back to the right
   202→        SEQ
   203→          num.pints := num.pints + 1
   204→          IF
   205→            num.pints = limit.pints
   206→              state := DEAD
   207→            num.pints >= drunk.pints
   208→              state := DRUNK
   209→            TRUE
   210→              SKIP
   211→          CLAIM out!
   212→            SEQ
   213→              out ! movecursor; punter.bcount; my.y
   214→              out ! number; num.pints
   215→          SEQ i = 0 FOR INT (punter.right - punter.left)
   216→            SEQ
   217→              CLAIM out!
   218→                SEQ
   219→                  out ! movecursor; my.x; my.y
   220→                  out ! blank
   221→                  my.x := (my.x + 1)
   222→                  out ! movecursor; my.x; my.y
   223→                  out ! happy; state
   224→              tim ? t
   225→              tim ? AFTER (t PLUS speed.const)
   226→        --}}}
   227→    --}}}
   228→:
   229→--}}}
   230→
   231→--{{{  PROC punters ([]CHAN BYTE req!, rsp?, SHARED CHAN O.REQ d.chan!, CHAN BOOL kill.keyboard!, kill.bar!)
   232→PROC punters ([]CHAN INT req!, rsp?, SHARED CHAN O.REQ d.chan!,
   233→              CHAN BOOL kill.keyboard!, kill.bar!)
   234→
   235→  INT seed:                        -- random number seed
   236→  
   237→  SEQ
   238→
   239→    --{{{  Set random seed
   240→    TIMER tim:
   241→    SEQ
   242→      tim ? seed
   243→      seed := (seed >> 2) + 42
   244→    --}}}
   245→    
   246→    PAR i = 0 FOR num.punters
   247→      punter (i, seed, req[i]!, rsp[i]?, d.chan!)
   248→
   249→    kill.keyboard ! TRUE           -- kill off the others
   250→    kill.bar ! TRUE                -- carefully ...
   251→    CLAIM d.chan!
   252→      d.chan ! terminate             -- order!
   253→
   254→:
   255→--}}}
   256→
   257→--{{{  PROC display (CHAN O.REQ in?, CHAN BYTE out!)
   258→PROC display (CHAN O.REQ in?, CHAN BYTE out!)
   259→  [256]BYTE str:
   260→  INITIAL BOOL running IS TRUE:
   261→  SEQ
   262→    cursor.invisible (out!)
   263→    erase.screen (out!)
   264→    WHILE running
   265→      SEQ
   266→        in ? CASE
   267→          BYTE x, y:
   268→          movecursor; x; y
   269→            cursor.x.y (x, y, out!)
   270→          INT len:
   271→          string; len::str
   272→            out.string ([str FOR len], 0, out!)
   273→          INT n:
   274→          number; n
   275→            out.int (n, 0, out!)
   276→          blank
   277→            out.string (punter.blank, 0, out!)
   278→          INT s:
   279→          happy; s
   280→            SEQ
   281→              out.string (punter.happy[s], 0, out!)
   282→              out ! '*c'
   283→          INT s:
   284→          thirsty; s
   285→            SEQ
   286→              out.string (punter.thirsty[s], 0, out!)
   287→              out ! '*c'
   288→          INT s:
   289→          sad; s
   290→            SEQ
   291→              out.string (punter.sad[s], 0, out!)
   292→              out ! '*c'
   293→          bar.blank
   294→            SEQ
   295→              out.string (barkeep.blank, 0, out!)
   296→              out ! '*c'
   297→          bar.happy
   298→            SEQ
   299→              out.string (barkeep.happy, 0, out!)
   300→              out ! '*c'
   301→          terminate
   302→            SEQ
   303→              running := FALSE
   304→              cursor.x.y (1, help.y - 1, out!)
   305→              erase.eos (out!)
   306→        out ! FLUSH
   307→    cursor.visible (out!)
   308→:
   309→--}}}
   310→
   311→--{{{  PROC bar.brain ([]CHAN BYTE in?, CHAN BYTE out!, CHAN INT mode.change?, CHAN BOOL terminate?)
   312→PROC bar.brain ([]CHAN INT in?, CHAN INT out!,
   313→                CHAN INT mode.change?, CHAN BOOL terminate?)
   314→  VAL INT s IS SIZE in:
   315→  INITIAL INT local.alt.mode IS PRI.ALT:
   316→  INITIAL INT favourite IS 0:
   317→  INITIAL BOOL running IS TRUE:
   318→  WHILE running
   319→    CASE local.alt.mode
   320→      PRI.ALT
   321→        PRI ALT
   322→          BOOL any:
   323→          terminate ? any
   324→            running := FALSE
   325→          mode.change ? local.alt.mode
   326→            SKIP
   327→          PRI ALT i = 0 FOR s
   328→            INT any:
   329→            in[i] ? any
   330→              out ! any
   331→      FAIRISH.ALT
   332→        PRI ALT
   333→          BOOL any:
   334→          terminate ? any
   335→            running := FALSE
   336→          mode.change ? local.alt.mode
   337→            SKIP
   338→          PRI ALT j = favourite FOR s
   339→            VAL INT X IS (j \ s):
   340→            INT any:
   341→            in[X] ? any
   342→              SEQ
   343→                out ! any
   344→                favourite := ((favourite + 1) \ s)
   345→      FAIR.ALT
   346→        PRI ALT
   347→          BOOL any:
   348→          terminate ? any
   349→            running := FALSE
   350→          mode.change ? local.alt.mode
   351→            SKIP
   352→          PRI ALT i = favourite FOR s
   353→            VAL INT X IS (i \ s):
   354→            INT any:
   355→            in[X] ? any
   356→              SEQ
   357→                out ! any
   358→                favourite := (X + 1)
   359→:
   360→--}}}
   361→
   362→--{{{  PROC bar.body (CHAN BYTE in?, []CHAN BYTE responses!, SHARED CHAN O.REQ out!, CHAN BOOL terminate?)
   363→PROC bar.body (CHAN INT in?, []CHAN INT responses!, SHARED CHAN O.REQ out!,
   364→               CHAN BOOL terminate?)
   365→  VAL INT bar.delay IS milliseconds (20):
   366→  VAL INT bar.anim.delay IS milliseconds (20):
   367→
   368→  PROC barkeep.from.to (VAL BYTE start.y, end.y)
   369→    TIMER tim:
   370→    INT t:
   371→    SEQ
   372→      tim ? t
   373→      tim ? AFTER (t PLUS bar.anim.delay)
   374→      CLAIM out!
   375→        SEQ
   376→          out ! movecursor; barkeep.x; start.y
   377→          out ! bar.blank
   378→          out ! movecursor; barkeep.x; end.y
   379→          out ! bar.happy
   380→  :
   381→
   382→  INITIAL BYTE barkeep.y IS punter.top:
   383→  INITIAL BOOL running IS TRUE:
   384→  INITIAL BOOL serving IS FALSE:
   385→  TIMER tim:
   386→  INT wake.up:
   387→  SEQ
   388→    CLAIM out!
   389→      SEQ
   390→        out ! movecursor; barkeep.x; barkeep.y
   391→        out ! bar.happy
   392→    tim ? wake.up
   393→    wake.up := wake.up PLUS bar.delay
   394→    WHILE running
   395→      PRI ALT
   396→        BOOL any:
   397→        terminate ? any
   398→          running := FALSE
   399→        (NOT serving) & tim ? AFTER wake.up
   400→          serving := TRUE
   401→        INT n:
   402→        serving & in ? n
   403→          BYTE new.y:
   404→          SEQ
   405→            new.y := ((BYTE n) + punter.top)
   406→            IF
   407→              new.y < barkeep.y
   408→                SEQ i = 0 FOR INT (barkeep.y - new.y)
   409→                  VAL BYTE bi IS BYTE i:
   410→                  barkeep.from.to (barkeep.y - bi,
   411→                                   barkeep.y - (bi + 1))
   412→              new.y > barkeep.y
   413→                SEQ i = INT barkeep.y FOR INT (new.y - barkeep.y)
   414→                  VAL BYTE bi IS BYTE i:
   415→                  barkeep.from.to (bi, bi + 1)
   416→              TRUE
   417→                SKIP
   418→            barkeep.y := new.y
   419→            responses[n] ! 0
   420→            serving := FALSE
   421→            tim ? wake.up
   422→            wake.up := wake.up PLUS bar.delay
   423→:
   424→--}}}
   425→
   426→--{{{  PROC bar.tender ([]CHAN BYTE req?, rsp!, CHAN INT mode.change?, SHARED CHAN O.REQ d.chan!, CHAN BOOL kill?)
   427→PROC bar.tender ([]CHAN INT req?, rsp!, CHAN INT mode.change?,
   428→                 SHARED CHAN O.REQ d.chan!, CHAN BOOL kill?)
   429→  CHAN INT spinal.chord:
   430→  CHAN BOOL kill.brain, kill.body:
   431→  PAR
   432→    --{{{  terminator
   433→    BOOL any:
   434→    SEQ
   435→      kill ? any
   436→      kill.brain ! any    -- order is
   437→      kill.body ! any     -- important
   438→    --}}}
   439→    bar.brain (req?, spinal.chord!, mode.change?, kill.brain?)
   440→    bar.body (spinal.chord?, rsp!, d.chan!, kill.body?)
   441→:
   442→--}}}
   443→
   444→--{{{  PROC keyboard.reader (CHAN BYTE in?, CHAN INT out!, CHAN BOOL terminate?)
   445→PROC keyboard.reader (CHAN BYTE in?, CHAN INT out!, CHAN BOOL terminate?)
   446→  INITIAL BOOL running IS TRUE:
   447→  WHILE running
   448→    PRI ALT
   449→      BOOL any:
   450→      terminate ? any
   451→        running := FALSE
   452→      BYTE ch:
   453→      in ? ch
   454→        IF
   455→          (ch >= '0') AND (ch <= '2')
   456→            CASE ch - '0'
   457→              0
   458→                out ! PRI.ALT
   459→              1
   460→                out ! FAIRISH.ALT
   461→              2
   462→                out ! FAIR.ALT
   463→          TRUE
   464→            SKIP
   465→:
   466→--}}}
   467→
   468→--{{{  PROC bar (CHAN BYTE keyboard?, screen!, error!)
   469→PROC bar (CHAN BYTE keyboard?, screen!, error!)
   470→
   471→  SHARED ! CHAN O.REQ d.chan:
   472→  [num.punters]CHAN INT req:
   473→  [num.punters]CHAN INT rsp:
   474→  CHAN INT mode.change:
   475→  CHAN BOOL kill.keyboard:
   476→  CHAN BOOL kill.bar:
   477→  PAR
   478→    display (d.chan?, screen!)
   479→    SEQ
   480→      draw.bar (d.chan!)
   481→      PAR
   482→        punters (req!, rsp?, d.chan!, kill.keyboard!, kill.bar!)
   483→        keyboard.reader (keyboard?, mode.change!, kill.keyboard?)
   484→        bar.tender (req?, rsp!, mode.change?, d.chan!, kill.bar?)
   485→:
   486→--}}}
   487→
   488→

Now I can see the pattern. The parser at line 169 checks if a type keyword (INT_TYPE, etc.) is followed by FUNCTION or FUNC, but when INLINE appears between them (e.g., INT INLINE FUNCTION), it doesn't match and falls through to variable declaration parsing.

Let me look at the exact parser code around that area more carefully.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 155,
  "limit": 30
}
   155→}
   156→
   157→func (p *Parser) parseStatement() ast.Statement {
   158→	// Skip newlines
   159→	for p.curTokenIs(lexer.NEWLINE) {
   160→		p.nextToken()
   161→	}
   162→
   163→	switch p.curToken.Type {
   164→	case lexer.VAL:
   165→		return p.parseAbbreviation()
   166→	case lexer.INITIAL:
   167→		return p.parseInitialDecl()
   168→	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
   169→		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
   170→			return p.parseFuncDecl()
   171→		}
   172→		return p.parseVarDeclOrAbbreviation()
   173→	case lexer.LBRACKET:
   174→		return p.parseArrayDecl()
   175→	case lexer.CHAN:
   176→		return p.parseChanDecl()
   177→	case lexer.PROTOCOL:
   178→		return p.parseProtocolDecl()
   179→	case lexer.RECORD:
   180→		return p.parseRecordDecl()
   181→	case lexer.TIMER:
   182→		return p.parseTimerDecl()
   183→	case lexer.SEQ:
   184→		return p.parseSeqBlock()
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2155,
  "limit": 30
}
  2155→		return nil
  2156→	}
  2157→
  2158→	return call
  2159→}
  2160→
  2161→func (p *Parser) parseFuncDecl() *ast.FuncDecl {
  2162→	fn := &ast.FuncDecl{
  2163→		Token:       p.curToken,
  2164→		ReturnTypes: []string{p.curToken.Literal},
  2165→	}
  2166→
  2167→	// Parse additional return types for multi-result functions: INT, INT FUNCTION
  2168→	for p.peekTokenIs(lexer.COMMA) {
  2169→		p.nextToken() // consume comma
  2170→		p.nextToken() // move to next type
  2171→		fn.ReturnTypes = append(fn.ReturnTypes, p.curToken.Literal)
  2172→	}
  2173→
  2174→	// Consume FUNCTION keyword
  2175→	p.nextToken()
  2176→
  2177→	if !p.expectPeek(lexer.IDENT) {
  2178→		return nil
  2179→	}
  2180→	fn.Name = p.curToken.Literal
  2181→
  2182→	if !p.expectPeek(lexer.LPAREN) {
  2183→		return nil
  2184→	}
Thinking

Now I understand the issue clearly. The fix needs two parts:

  1. Lexer: Add INLINE as a keyword token
  2. Parser:
  3. In parseStatement(), when we see a type keyword (INT_TYPE, etc.), also check if the peek token is INLINE (meaning INT INLINE FUNCTION ...)
  4. In parseFuncDecl(), skip the INLINE keyword if present before FUNCTION

Let me implement this. The approach is simple: 1. Add INLINE token to lexer 2. In the parser's parseStatement(), add p.peekTokenIs(lexer.INLINE) to the condition that routes to parseFuncDecl()

  1. In parseFuncDecl(), after parsing return types and before consuming the FUNCTION keyword, skip INLINE if present

I see the issue now—when the lexer doesn't recognize INLINE as a keyword yet, it comes through as an IDENT token, so the condition checking for FUNCTION or COMMA fails. Once I add INLINE as a proper keyword token, the parser will be able to detect it in the peek position and route correctly to function declaration parsing.

I need to add the INLINE keyword to the lexer, update the condition in parseStatement() to check for it, and then handle skipping it in parseFuncDecl() after the multi-return-type loop finishes.

I have a clear understanding of the issue and the fix. The INLINE keyword between the return type and FUNCTION isn't recognized, so the parser falls through to variable declaration parsing. I need to:

  1. Add INLINE as a keyword token in the lexer
  2. Route INT INLINE FUNCTION ... to parseFuncDecl() in the parser
  3. Skip INLINE in parseFuncDecl() before consuming FUNCTION

Let me also add tests.

✏️ Edit token.go
/home/david/projects/code-associates/occam2go/lexer/token.go
	PLUS_KW  // PLUS (modular addition keyword, distinct from + symbol)
	MINUS_KW // MINUS (modular subtraction keyword, distinct from - symbol)
	TIMES    // TIMES (modular multiplication keyword)
	keyword_end
+
	INLINE   // INLINE (function modifier, ignored for transpilation)
	PLUS_KW  // PLUS (modular addition keyword, distinct from + symbol)
	MINUS_KW // MINUS (modular subtraction keyword, distinct from - symbol)
	TIMES    // TIMES (modular multiplication keyword)
	keyword_end
The file /home/david/projects/code-associates/occam2go/lexer/token.go has been updated successfully.
✏️ Edit token.go
/home/david/projects/code-associates/occam2go/lexer/token.go
	RETYPES:    "RETYPES",
	PLUS_KW:    "PLUS",
+
	RETYPES:    "RETYPES",
	INLINE:     "INLINE",
	PLUS_KW:    "PLUS",
The file /home/david/projects/code-associates/occam2go/lexer/token.go has been updated successfully.
✏️ Edit token.go
/home/david/projects/code-associates/occam2go/lexer/token.go
	"RETYPES":  RETYPES,
	"PLUS":     PLUS_KW,
+
	"RETYPES":  RETYPES,
	"INLINE":   INLINE,
	"PLUS":     PLUS_KW,
The file /home/david/projects/code-associates/occam2go/lexer/token.go has been updated successfully.

Now update the parser — first the routing in parseStatement():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
			return p.parseFuncDecl()
		}
+
	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) || p.peekTokenIs(lexer.INLINE) {
			return p.parseFuncDecl()
		}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update parseFuncDecl() to skip INLINE before consuming FUNCTION:

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	// Consume FUNCTION keyword
	p.nextToken()
+
	// Skip INLINE modifier if present (optimization hint, ignored for transpilation)
	if p.peekTokenIs(lexer.INLINE) {
		p.nextToken()
	}

	// Consume FUNCTION keyword
	p.nextToken()
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now let me add tests. Let me first look at the existing parser tests for function declarations to follow the pattern.

Grep
{
  "pattern": "FuncDecl|parseFuncDecl|FUNCTION",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "output_mode": "content",
  "-n": true,
  "context": 2
}
740-}
741-
742:func TestFuncDeclIS(t *testing.T) {
743:	input := `INT FUNCTION square(VAL INT x)
744-  IS x * x
745-`
--
753-	}
754-
755:	fn, ok := program.Statements[0].(*ast.FuncDecl)
756-	if !ok {
757:		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
758-	}
759-
--
783-}
784-
785:func TestFuncDeclValof(t *testing.T) {
786:	input := `INT FUNCTION max(VAL INT a, VAL INT b)
787-  INT result:
788-  VALOF
--
803-	}
804-
805:	fn, ok := program.Statements[0].(*ast.FuncDecl)
806-	if !ok {
807:		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
808-	}
809-
--
830-}
831-
832:func TestMultiResultFuncDecl(t *testing.T) {
833:	input := `INT, INT FUNCTION swap(VAL INT a, VAL INT b)
834-  INT x, y:
835-  VALOF
--
848-	}
849-
850:	fn, ok := program.Statements[0].(*ast.FuncDecl)
851-	if !ok {
852:		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
853-	}
854-
--
2669-}
2670-
2671:func TestNestedFuncDecl(t *testing.T) {
2672-	input := `PROC compute(VAL INT n)
2673:  INT FUNCTION double(VAL INT x)
2674-    IS x * 2
2675-  SEQ
--
2690-	}
2691-
2692:	// Body should have 2 statements: nested FuncDecl, SeqBlock
2693-	if len(proc.Body) != 2 {
2694-		t.Fatalf("expected 2 body statements, got %d", len(proc.Body))
2695-	}
2696-
2697:	fn, ok := proc.Body[0].(*ast.FuncDecl)
2698-	if !ok {
2699:		t.Fatalf("expected nested FuncDecl, got %T", proc.Body[0])
2700-	}
2701-	if fn.Name != "double" {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 742,
  "limit": 45
}
   742→func TestFuncDeclIS(t *testing.T) {
   743→	input := `INT FUNCTION square(VAL INT x)
   744→  IS x * x
   745→`
   746→	l := lexer.New(input)
   747→	p := New(l)
   748→	program := p.ParseProgram()
   749→	checkParserErrors(t, p)
   750→
   751→	if len(program.Statements) != 1 {
   752→		t.Fatalf("expected 1 statement, got %d", len(program.Statements))
   753→	}
   754→
   755→	fn, ok := program.Statements[0].(*ast.FuncDecl)
   756→	if !ok {
   757→		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
   758→	}
   759→
   760→	if len(fn.ReturnTypes) != 1 || fn.ReturnTypes[0] != "INT" {
   761→		t.Errorf("expected return types [INT], got %v", fn.ReturnTypes)
   762→	}
   763→
   764→	if fn.Name != "square" {
   765→		t.Errorf("expected name 'square', got %s", fn.Name)
   766→	}
   767→
   768→	if len(fn.Params) != 1 {
   769→		t.Fatalf("expected 1 param, got %d", len(fn.Params))
   770→	}
   771→
   772→	if fn.Params[0].Name != "x" || fn.Params[0].Type != "INT" || !fn.Params[0].IsVal {
   773→		t.Errorf("expected VAL INT x, got %+v", fn.Params[0])
   774→	}
   775→
   776→	if len(fn.ResultExprs) != 1 {
   777→		t.Fatalf("expected 1 result expression, got %d", len(fn.ResultExprs))
   778→	}
   779→
   780→	if len(fn.Body) != 0 {
   781→		t.Errorf("expected empty body for IS form, got %d statements", len(fn.Body))
   782→	}
   783→}
   784→
   785→func TestFuncDeclValof(t *testing.T) {
   786→	input := `INT FUNCTION max(VAL INT a, VAL INT b)

Good, I have the test pattern. Now let me find where to insert the new test — right after TestFuncDeclIS:

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
func TestFuncDeclValof(t *testing.T) {
+
func TestInlineFuncDecl(t *testing.T) {
	input := `INT INLINE FUNCTION seconds(VAL INT s)
  INT ticks:
  VALOF
    ticks := s * 1000000
    RESULT ticks
:
`
	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))
	}

	fn, ok := program.Statements[0].(*ast.FuncDecl)
	if !ok {
		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
	}

	if len(fn.ReturnTypes) != 1 || fn.ReturnTypes[0] != "INT" {
		t.Errorf("expected return types [INT], got %v", fn.ReturnTypes)
	}

	if fn.Name != "seconds" {
		t.Errorf("expected name 'seconds', got %s", fn.Name)
	}

	if len(fn.Params) != 1 {
		t.Fatalf("expected 1 param, got %d", len(fn.Params))
	}

	if fn.Params[0].Name != "s" || fn.Params[0].Type != "INT" || !fn.Params[0].IsVal {
		t.Errorf("expected VAL INT s, got %+v", fn.Params[0])
	}
}

func TestInlineFuncDeclIS(t *testing.T) {
	input := `INT INLINE FUNCTION double(VAL INT x)
  IS x * 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))
	}

	fn, ok := program.Statements[0].(*ast.FuncDecl)
	if !ok {
		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
	}

	if fn.Name != "double" {
		t.Errorf("expected name 'double', got %s", fn.Name)
	}

	if len(fn.ResultExprs) != 1 {
		t.Fatalf("expected 1 result expression, got %d", len(fn.ResultExprs))
	}
}

func TestFuncDeclValof(t *testing.T) {
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.

Now let's run the tests:

$ Bash
Run all tests
go test ./... 2>&1
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	15.784s
ok  	github.com/codeassociates/occam2go/lexer	0.001s
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	0.002s
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Let me also verify the fix works on the actual bar.occ file from the issue:

$ Bash
Build and test transpiling bar.occ
go build -o /tmp/occam2go . && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/bar_out.go kroc/modules/course/examples/bar.occ 2>&1 | head -20
Parse errors:
  kroc/modules/course/examples/bar.occ:77: expected variant tag name, got :
  kroc/modules/course/examples/bar.occ:77: unexpected token: :
  kroc/modules/course/examples/bar.occ:77: unexpected token in expression: ]
  kroc/modules/course/examples/bar.occ:77: expected ], got BYTE
  kroc/modules/course/examples/bar.occ:77: expected IDENT, got NEWLINE
  kroc/modules/course/examples/bar.occ:78: unexpected token: ;
  kroc/modules/course/examples/bar.occ:78: expected IDENT, got NEWLINE
  kroc/modules/course/examples/bar.occ:79: unexpected token: ;
  kroc/modules/course/examples/bar.occ:79: expected IDENT, got NEWLINE
  kroc/modules/course/examples/bar.occ:80: unexpected token: ;
  kroc/modules/course/examples/bar.occ:80: expected IDENT, got NEWLINE
  kroc/modules/course/examples/bar.occ:81: unexpected token: ;
  kroc/modules/course/examples/bar.occ:81: expected IDENT, got NEWLINE
  kroc/modules/course/examples/bar.occ:86: unexpected token: :
  kroc/modules/course/examples/bar.occ:109: expected type after VAL, got [
  kroc/modules/course/examples/bar.occ:109: unexpected token: ]
  kroc/modules/course/examples/bar.occ:110: expected type after VAL, got [
  kroc/modules/course/examples/bar.occ:110: unexpected token: ]
  kroc/modules/course/examples/bar.occ:111: expected type after VAL, got [

The INLINE FUNCTION error from line 29 is gone — the remaining errors are from other unsupported features in bar.occ (variant protocol string; INT::[]BYTE, SHARED CHAN, etc.) which are separate issues. The fix is working correctly.

Now let me update TODO.md as recommended in the workflow:

Grep
{
  "pattern": "INLINE",
  "path": "/home/david/projects/code-associates/occam2go/TODO.md",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "What's Implemented|Implemented",
  "path": "/home/david/projects/code-associates/occam2go/TODO.md",
  "output_mode": "content",
  "-n": true
}
3:## Fully Implemented
81:## Not Yet Implemented
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/TODO.md",
  "offset": 1,
  "limit": 85
}
     1→# occam2go — Implementation Status
     2→
     3→## Fully Implemented
     4→
     5→### Core Constructs
     6→- **SEQ** — Sequential execution, with replicators (`SEQ i = 0 FOR n`) and optional STEP
     7→- **PAR** — Parallel execution via goroutines + sync.WaitGroup, with replicators
     8→- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process)
     9→- **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies
    10→- **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies
    11→- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, and multi-statement bodies
    12→- **SKIP** — No-op process
    13→- **STOP** — Error + deadlock
    14→
    15→### Data Types & Declarations
    16→- **INT, BYTE, BOOL, REAL, REAL32, REAL64** — Scalar types (REAL/REAL64 map to float64, REAL32 maps to float32)
    17→- **Variable declarations** — `INT x, y, z:`
    18→- **Arrays** — `[n]TYPE arr:` with index expressions
    19→- **Channels** — `CHAN OF TYPE c:` with send (`!`) and receive (`?`); `CHAN BYTE` shorthand (without `OF`)
    20→- **Channel arrays** — `[n]CHAN OF TYPE cs:` with indexed send/receive and `[]CHAN OF TYPE` proc params
    21→- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only); direction annotations at call sites (`out!`, `in?`) accepted and ignored
    22→- **Timers** — `TIMER tim:` with reads and `AFTER` expressions
    23→- **Abbreviations** — `VAL INT x IS 1:`, `INT y IS z:`, untyped `VAL x IS expr:` — named constants and aliases
    24→- **INITIAL declarations** — `INITIAL INT x IS 42:` — mutable variables with initial values
    25→- **Byte literals** — `'A'`, `'0'` with occam escape sequences (`*n`, `*c`, `*t`)
    26→- **Hex integer literals** — `#FF`, `#80000000`
    27→
    28→### Procedures & Functions
    29→- **PROC** — Declaration with VAL, reference, CHAN OF, and open array (`[]TYPE`) parameters
    30→- **PROC calls** — With automatic `&`/`*` for reference params, pass-through for channels
    31→- **FUNCTION (IS form)** — `INT FUNCTION square(VAL INT x) IS x * x`
    32→- **FUNCTION (VALOF form)** — Local declarations + VALOF body + RESULT
    33→- **Multi-result FUNCTIONs** — `INT, INT FUNCTION f(...)` returning multiple values via `RESULT a, b`
    34→- **Nested PROCs/FUNCTIONs** — Local definitions inside a PROC body, compiled as Go closures
    35→- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
    36→- **Built-in print** — `print.int`, `print.bool`, `print.string`, `print.newline`
    37→
    38→### Expressions & Operators
    39→- **Arithmetic** — `+`, `-`, `*`, `/`, `\` (modulo)
    40→- **Comparison** — `=`, `<>`, `<`, `>`, `<=`, `>=`
    41→- **Logical** — `AND`, `OR`, `NOT`
    42→- **Bitwise** — `/\`, `\/`, `><`, `~`, `<<`, `>>`
    43→- **AFTER** — As boolean expression (maps to `>`)
    44→- **Parenthesized expressions**
    45→- **Array indexing** — `arr[i]`, `arr[expr]`
    46→- **String literals** — Double-quoted strings
    47→- **Type conversions** — `INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`
    48→- **Checked arithmetic** — `PLUS`, `MINUS`, `TIMES` — modular (wrapping) operators
    49→- **MOSTNEG/MOSTPOS** — Type min/max constants for INT, BYTE, REAL32, REAL64
    50→- **SIZE operator** — `SIZE arr`, `SIZE "str"` maps to `len()`
    51→- **Array slices** — `[arr FROM n FOR m]` with slice assignment
    52→- **Array literals** — `[1, 2, 3]` — inline array/table expressions
    53→- **Multi-assignment** — `a, b := f(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`
    54→- **Multi-line expression continuation** — Binary operators and `:=` at end of line continue expression on next line
    55→
    56→### Protocols
    57→- **Simple** — `PROTOCOL SIG IS INT` (type alias)
    58→- **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)
    59→- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)
    60→
    61→### Records
    62→- **RECORD** — Struct types with field access via bracket syntax (`p[x]`)
    63→
    64→### Type Reinterpretation & Intrinsics
    65→- **RETYPES** — Bit-level type reinterpretation (`VAL INT X RETYPES X :` for float32→int, `VAL [2]INT X RETYPES X :` for float64→int pair)
    66→- **Transputer intrinsics** — `LONGPROD`, `LONGDIV`, `LONGSUM`, `LONGDIFF`, `NORMALISE`, `SHIFTLEFT`, `SHIFTRIGHT` — extended-precision arithmetic as Go helper functions
    67→- **CAUSEERROR** — Error-raising primitive, maps to `panic("CAUSEERROR")`
    68→
    69→### Preprocessor
    70→- **`#IF` / `#ELSE` / `#ENDIF`** — Conditional compilation with `TRUE`, `FALSE`, `DEFINED()`, `NOT`, equality
    71→- **`#DEFINE`** — Symbol definition
    72→- **`#INCLUDE`** — File inclusion with search paths and include guards
    73→- **`#COMMENT` / `#PRAGMA` / `#USE`** — Ignored (blank lines)
    74→- **Predefined symbols** — `TARGET.BITS.PER.WORD = 64`
    75→
    76→### Tooling
    77→- **gen-module** — Generate `.module` files from KRoC SConscript build files
    78→
    79→---
    80→
    81→## Not Yet Implemented
    82→
    83→### Required for shared_screen module (extends course module)
    84→
    85→| Feature | Notes | Used in |
✏️ Edit TODO.md
/home/david/projects/code-associates/occam2go/TODO.md
- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
+
- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
- **INLINE modifier** — `INT INLINE FUNCTION f(...)` — accepted and ignored (optimization hint only)
The file /home/david/projects/code-associates/occam2go/TODO.md has been updated successfully.

Now update CLAUDE.md to document the mapping:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "offset": 74,
  "limit": 10
}
    74→| `c ! expr` | `c <- expr` |
    75→| `c ? x` | `x = <-c` |
    76→| `PROC name(...)` | `func name(...)` |
    77→| `INT FUNCTION name(...) IS expr` | `func name(...) int { return expr }` |
    78→| `INT, INT FUNCTION name(...)` | `func name(...) (int, int) { ... }` |
    79→| `RESULT expr1, expr2` | `return expr1, expr2` |
    80→| `a, b := func(...)` | `a, b = func(...)` (multi-assignment) |
    81→| `x[0], x[1] := x[1], x[0]` | `x[0], x[1] = x[1], x[0]` (indexed multi-assignment) |
    82→| `TIMER` / `tim ? t` | `time.Now().UnixMicro()` |
    83→| `=` / `<>` | `==` / `!=` |
✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
| `INT FUNCTION name(...) IS expr` | `func name(...) int { return expr }` |
+
| `INT FUNCTION name(...) IS expr` | `func name(...) int { return expr }` |
| `INT INLINE FUNCTION name(...)` | `func name(...) int { ... }` (INLINE ignored) |
The file /home/david/projects/code-associates/occam2go/CLAUDE.md has been updated successfully.

Also update the "What's Implemented" section in CLAUDE.md:

Grep
{
  "pattern": "KRoC-style colon terminators",
  "path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "output_mode": "content",
  "-n": true
}
174:[Omitted long matching line]
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "offset": 170,
  "limit": 5
}
   170→6. **Documentation**: Update TODO.md to reflect support for the new feature.
   171→
   172→## What's Implemented
   173→
   174→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, and multi-statement bodies with scoped declarations), 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 and `[]CHAN OF TYPE` proc params), 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), replicators on SEQ/PAR/IF (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), 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, 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")`).
✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
KRoC-style colon terminators on PROC/FUNCTION (optional)
+
KRoC-style colon terminators on PROC/FUNCTION (optional), INLINE function modifier (accepted and ignored)
The file /home/david/projects/code-associates/occam2go/CLAUDE.md has been updated successfully.

Let me run the tests one more time to make sure everything is clean:

$ Bash
Run all tests
go test ./... 2>&1
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	(cached)
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Here's a summary of the changes:

Problem: INT INLINE FUNCTION seconds(VAL INT s) caused parse error expected :, got FUNCTION because the parser didn't recognize INLINE between the return type and FUNCTION.

Fix (3 files):

  1. lexer/token.go — Added INLINE as a keyword token so it's recognized instead of being treated as an identifier.

  2. parser/parser.go — Two changes:

  3. parseStatement(): Added p.peekTokenIs(lexer.INLINE) to the condition that routes type keywords to parseFuncDecl() (line 169)
  4. parseFuncDecl(): Added a check to skip the INLINE token if present before consuming the FUNCTION keyword

  5. parser/parser_test.go — Added two new tests:

  6. TestInlineFuncDecl — VALOF form with INT INLINE FUNCTION
  7. TestInlineFuncDeclIS — IS form with INT INLINE FUNCTION

Documentation updated: TODO.md and CLAUDE.md both updated to reflect the new feature.

The INLINE modifier is purely an optimization hint and is ignored for transpilation — the function is parsed and generated exactly as if INLINE were absent, as agreed in the issue discussion.

Thank you. Please create a PR for this fix.

$ Bash
Show working tree status
git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   CLAUDE.md
	modified:   TODO.md
	modified:   lexer/token.go
	modified:   parser/parser.go
	modified:   parser/parser_test.go

no changes added to commit (use "git add" and/or "git commit -a")
$ Bash
Show staged and unstaged changes
git diff
diff --git a/CLAUDE.md b/CLAUDE.md
index b921039..7e50fdd 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -75,6 +75,7 @@ Six packages, one pipeline:
 | `c ? x` | `x = <-c` |
 | `PROC name(...)` | `func name(...)` |
 | `INT FUNCTION name(...) IS expr` | `func name(...) int { return expr }` |
+| `INT INLINE FUNCTION name(...)` | `func name(...) int { ... }` (INLINE ignored) |
 | `INT, INT FUNCTION name(...)` | `func name(...) (int, int) { ... }` |
 | `RESULT expr1, expr2` | `return expr1, expr2` |
 | `a, b := func(...)` | `a, b = func(...)` (multi-assignment) |
@@ -170,7 +171,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, and multi-statement bodies with scoped declarations), 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 and `[]CHAN OF TYPE` proc params), 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), replicators on SEQ/PAR/IF (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), 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, 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, IF, WHILE, CASE, ALT (with guards, timer timeouts, and multi-statement bodies with scoped declarations), 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 and `[]CHAN OF TYPE` proc params), 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 (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), 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, 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
 
diff --git a/TODO.md b/TODO.md
index 15ad2f8..79ee539 100644
--- a/TODO.md
+++ b/TODO.md
@@ -33,6 +33,7 @@
 - **Multi-result FUNCTIONs** — `INT, INT FUNCTION f(...)` returning multiple values via `RESULT a, b`
 - **Nested PROCs/FUNCTIONs** — Local definitions inside a PROC body, compiled as Go closures
 - **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
+- **INLINE modifier** — `INT INLINE FUNCTION f(...)` — accepted and ignored (optimization hint only)
 - **Built-in print** — `print.int`, `print.bool`, `print.string`, `print.newline`
 
 ### Expressions & Operators
diff --git a/lexer/token.go b/lexer/token.go
index 0bafe0f..9bfa27a 100644
--- a/lexer/token.go
+++ b/lexer/token.go
@@ -91,6 +91,7 @@ const (
 	MOSTPOS_KW
 	INITIAL
 	RETYPES  // RETYPES (bit-level type reinterpretation)
+	INLINE   // INLINE (function modifier, ignored for transpilation)
 	PLUS_KW  // PLUS (modular addition keyword, distinct from + symbol)
 	MINUS_KW // MINUS (modular subtraction keyword, distinct from - symbol)
 	TIMES    // TIMES (modular multiplication keyword)
@@ -180,6 +181,7 @@ var tokenNames = map[TokenType]string{
 	MOSTPOS_KW: "MOSTPOS",
 	INITIAL:    "INITIAL",
 	RETYPES:    "RETYPES",
+	INLINE:     "INLINE",
 	PLUS_KW:    "PLUS",
 	MINUS_KW:   "MINUS",
 	TIMES:      "TIMES",
@@ -227,6 +229,7 @@ var keywords = map[string]TokenType{
 	"MOSTPOS":  MOSTPOS_KW,
 	"INITIAL":  INITIAL,
 	"RETYPES":  RETYPES,
+	"INLINE":   INLINE,
 	"PLUS":     PLUS_KW,
 	"MINUS":    MINUS_KW,
 	"TIMES":    TIMES,
diff --git a/parser/parser.go b/parser/parser.go
index df08c88..8fbda71 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -166,7 +166,7 @@ func (p *Parser) parseStatement() ast.Statement {
 	case lexer.INITIAL:
 		return p.parseInitialDecl()
 	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
-		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
+		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) || p.peekTokenIs(lexer.INLINE) {
 			return p.parseFuncDecl()
 		}
 		return p.parseVarDeclOrAbbreviation()
@@ -2171,6 +2171,11 @@ func (p *Parser) parseFuncDecl() *ast.FuncDecl {
 		fn.ReturnTypes = append(fn.ReturnTypes, p.curToken.Literal)
 	}
 
+	// Skip INLINE modifier if present (optimization hint, ignored for transpilation)
+	if p.peekTokenIs(lexer.INLINE) {
+		p.nextToken()
+	}
+
 	// Consume FUNCTION keyword
 	p.nextToken()
 
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 1213381..7ff9606 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -782,6 +782,72 @@ func TestFuncDeclIS(t *testing.T) {
 	}
 }
 
+func TestInlineFuncDecl(t *testing.T) {
+	input := `INT INLINE FUNCTION seconds(VAL INT s)
+  INT ticks:
+  VALOF
+    ticks := s * 1000000
+    RESULT ticks
+:
+`
+	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))
+	}
+
+	fn, ok := program.Statements[0].(*ast.FuncDecl)
+	if !ok {
+		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
+	}
+
+	if len(fn.ReturnTypes) != 1 || fn.ReturnTypes[0] != "INT" {
+		t.Errorf("expected return types [INT], got %v", fn.ReturnTypes)
+	}
+
+	if fn.Name != "seconds" {
+		t.Errorf("expected name 'seconds', got %s", fn.Name)
+	}
+
+	if len(fn.Params) != 1 {
+		t.Fatalf("expected 1 param, got %d", len(fn.Params))
+	}
+
+	if fn.Params[0].Name != "s" || fn.Params[0].Type != "INT" || !fn.Params[0].IsVal {
+		t.Errorf("expected VAL INT s, got %+v", fn.Params[0])
+	}
+}
+
+func TestInlineFuncDeclIS(t *testing.T) {
+	input := `INT INLINE FUNCTION double(VAL INT x)
+  IS x * 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))
+	}
+
+	fn, ok := program.Statements[0].(*ast.FuncDecl)
+	if !ok {
+		t.Fatalf("expected FuncDecl, got %T", program.Statements[0])
+	}
+
+	if fn.Name != "double" {
+		t.Errorf("expected name 'double', got %s", fn.Name)
+	}
+
+	if len(fn.ResultExprs) != 1 {
+		t.Fatalf("expected 1 result expression, got %d", len(fn.ResultExprs))
+	}
+}
+
 func TestFuncDeclValof(t *testing.T) {
 	input := `INT FUNCTION max(VAL INT a, VAL INT b)
   INT result:
$ Bash
Show recent commit messages
git log --oneline -5
a6f7350 Merge pull request #42 from codeassociates/feat/source-map-error-messages
9227d08 Add source file tracking to parser error messages
d6b665f Merge pull request #38 from codeassociates/docs/gen-module-limitation
cb5f541 Document gen-module regex-based limitation
e8c9709 Merge pull request #36 from codeassociates/fix/readme-course-module-step
$ Bash
Create feature branch
git checkout -b feat/inline-function-modifier
Switched to a new branch 'feat/inline-function-modifier'
$ Bash
Commit all changes
git add lexer/token.go parser/parser.go parser/parser_test.go CLAUDE.md TODO.md && git commit -m "$(cat <<'EOF'
Support INLINE function modifier

The INLINE keyword between return type and FUNCTION (e.g.
`INT INLINE FUNCTION seconds(...)`) is now recognized and
skipped as an optimization hint that has no effect on transpilation.

Closes #44

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
5 files changed, 78 insertions(+), 2 deletions(-)
$ Bash
Push branch to remote
git push -u origin feat/inline-function-modifier
remote: 
remote: Create a pull request for 'feat/inline-function-modifier' on GitHub by visiting:        
remote:      https://github.com/codeassociates/occam2go/pull/new/feat/inline-function-modifier        
remote: 
To associate-1.github.com:codeassociates/occam2go.git
 * [new branch]      feat/inline-function-modifier -> feat/inline-function-modifier
branch 'feat/inline-function-modifier' set up to track 'origin/feat/inline-function-modifier'.
$ Bash
Create pull request
gh pr create --title "Support INLINE function modifier" --body "$(cat <<'EOF'
## Summary

- Add `INLINE` as a recognized keyword token in the lexer
- Skip the `INLINE` modifier in the parser when it appears between return type(s) and `FUNCTION` (e.g. `INT INLINE FUNCTION seconds(...)`)
- The modifier is purely an optimization hint and has no effect on transpiled output — functions are generated identically with or without it

## Test plan

- [x] New parser tests: `TestInlineFuncDecl` (VALOF form) and `TestInlineFuncDeclIS` (IS form)
- [x] All existing tests pass (`go test ./...`)
- [x] Verified `bar.occ` no longer produces `expected :, got FUNCTION` error on lines 29/45/61

Closes #44

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
https://github.com/codeassociates/occam2go/pull/47

PR created: https://github.com/codeassociates/occam2go/pull/47

Thank you. I've approved the PR. Please merge.

$ Bash
Merge PR #47
gh pr merge 47 --merge
$ Bash
Check PR merge status
gh pr view 47 --json state,mergedAt
{"mergedAt":"2026-02-22T19:14:45Z","state":"MERGED"}

PR #47 has been merged. Let me switch back to main and pull.

$ Bash
Switch to main and pull latest
git checkout main && git pull
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
From associate-1.github.com:codeassociates/occam2go
   a6f7350..3cbf1cd  main       -> origin/main
Updating a6f7350..3cbf1cd
Fast-forward
 CLAUDE.md             |  3 ++-
 TODO.md               |  1 +
 lexer/token.go        |  3 +++
 parser/parser.go      |  7 +++++-
 parser/parser_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 78 insertions(+), 2 deletions(-)

Done. PR #47 is merged and main is up to date.

Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.

/exit exit