plato-367.md
August 2, 2026 · View on GitHub
Task
Primitive types — those that must be defined outside the Plato runtime — should be
declared with a dedicated primitive keyword instead of type, so the language is
honest about which types the compiler assumes to exist by name.
The primitive set (per Christopher, 2026-07-30): Number, Integer, Boolean, Character, String, Array, Buffer, List, Function0–Function9, Dynamic, Type.
Scope for this increment (explicitly per user):
- Grammar — accept
primitiveas a declaration keyword (parallel totype). - Stdlib — declare all of the above with
primitivein a single fileprimitives.plato(consolidating/replacing the current declarations). - Parses — the stdlib parses cleanly after the change.
Explicitly OUT of scope for now: linters, gates, ratchets, and C# code generation.
Current state (measured 2026-07-30)
- Grammar:
parakeet/Parakeet.Grammars/PlatoGrammar.cs:187—Typerule isUniqueKeyword.Optional() + Keyword("type") + .... CSTs are generated intoparakeet/Parakeet.Cst/PlatoGrammarCst*.cs(and mirrored inparakeet/output/). Parakeet grammar is NOT frozen, but parakeet is its own submodule — push it manually. - Stdlib declarations today:
stdlib/foundation/primitives.types.plato— Number, Integer, Boolean, String, Character, Dynamic, Object (Object is NOT on the primitive list; decide whether it stays a plaintypeor joins the list — surface the call, don't silently drop it).stdlib/foundation/primitives-arrays.types.plato— Array, Array2D , Array3D (only Array is on the list; Array2D/3D presumably stay type).- Buffer, List, Function0–9, Type are not declared anywhere in stdlib today —
they are compiler-assumed names (see
PlatoCompiler/Types/WriterPrimitiveNames.cs). This task makes them visible asprimitivedeclarations inprimitives.plato.
- Compiler: parser/symbol path must accept the new node kind —
PlatoCompiler/Symbols/SymbolFactory.csbuilds type definitions from the CST. The minimal path is:primitiveproduces the same TypeDefinition astype(perhaps with an IsPrimitive flag), so everything downstream is untouched.
Done means
-
PlatoGrammar.csacceptsprimitive Name<Params> implements ... { }declarations; CST regenerated; parakeet pushed manually. -
stdlib/foundation/primitives.platoexists and declares Number, Integer, Boolean, Character, String, Array, Buffer, List, Function0–Function9, Dynamic, Type with theprimitivekeyword; the superseded declarations are removed (no duplicate decls). - Whole stdlib parses cleanly (plato_check MCP or compiler parse stage — parse errors zero; downstream lint/ratchet noise expressly ignored this increment).
- Decision recorded (in this file) for Object, Array2D/3D, and what interfaces (if any) the newly-declared Buffer/List/FunctionN/Type implement.
Outcome (2026-07-30)
Grammar (parakeet 817e636)
public Rule PrimitiveKeyword => Node(Keyword("primitive"));
public Rule Type => Node(UniqueKeyword.Optional() + (PrimitiveKeyword | Keyword("type")) + ...);
One new CST leaf (CstPrimitiveKeyword) and one new filter on CstType; type is
untouched, so every existing declaration parses exactly as before. unique primitive List<T> works because UniqueKeyword still leads. Regenerated
Parakeet.Cst/PlatoGrammarCst{,Factory}.cs + output/PlatoGrammar/grammar.txt via
dotnet test Parakeet.Tests --filter GenerateCstCode / ~OutputDefinitions.
Compiler (Plato 571f077)
Modelled exactly on the existing unique modifier, one hop per stage:
CstType.PrimitiveKeyword.Present -> AstTypeDeclaration.IsPrimitiveDeclaration
(Plato.AST/Ast.cs, AstNodeFactory.cs) -> TypeDef.IsPrimitiveDeclaration
(PlatoCompiler/Symbols/Definitions.cs, SymbolFactory.cs). Kind stays
TypeKind.ConcreteType, deliberately: a primitive declaration is byte-for-byte the
same TypeDef a type declaration produced, so resolution, checking and every writer are
untouched by this increment. Note the name — TypeDef.IsPrimitiveDeclaration is NOT the
pre-existing TypeExtensions.IsPrimitive(), which tests Kind == TypeKind.Primitive.
Nothing reads the new flag yet; it is the hook for plato-365.
Decisions
- Object stays
type(inprimitives.types.plato, now the only declaration there). The compiler does not assumeObjectby name and it is not inWriterPrimitiveNames.All— it is an ordinary interop type whose declaration is its own authority. - Array2D/Array3D stay
type(inprimitives-arrays.types.plato). Same reason: only rank-1Array<T>is a compiler-assumed name. - Interface surfaces are carried over verbatim, not invented.
Number: Real;Integer: Whole, Bitwise;Boolean: Value, Orderable, Logical;Character: Value, Orderable;String: Value, Orderable, Countable;Array<T>: Indexable<T>.Dynamic,Type,List<T>,Buffer<T>andFunction0–9implement nothing —Dynamic/Typeare escape hatches with no interface surface, the builders are affine and reachable only throughintrinsics.library.plato, and the function types are structural. Adding interfaces to any of them is a separate, deliberate design call, not a side effect of this rename. uniqueis retained on List/Buffer (unique primitive List<T>):UniqueTypesand LINT006/007 key offIsUnique, and dropping it would change behaviour.- Two files became empty and were deleted:
primitives-builders.types.plato(its long affine-builder doc block moved intoprimitives.plato) andprimitives-functions.types.plato.
Known, accepted deviations
primitives.platohas no.types/.interfaces/.librarysuffix — per the task spec the primitive set is its own kind of declaration and gets its own name. Any file-suffix lint rule that objects is accepted noise.- It holds 20 top-level declarations, over the stdlib's "at most twelve per file" convention. Deliberate: the whole point is that the primitive set is visible in one place. Revisit if/when plato-365 shrinks the set.
Verification
dotnet run --project submodules/Plato/Plato.CLI -c Release -- lint submodules/Plato/stdlib/{foundation,geometry,graphics,future} --strict -> exit 0;
786 files, 0 parse errors, 0 symbol resolution errors, 0 lint Errors,
ratchet 229 (unchanged). Info findings 2577 -> 2583 (LINT010, Info severity, excluded
from the ratchet). Out of scope and not run: C# codegen, the forward-conformance suite,
check-all.ps1. Generated/ does not compile, but that is pre-existing plato-365a Angle
work from a concurrent session, unrelated to this change.
Notes
- Related: plato-365 (retire non-scalar primitives / PrimitiveTypes =
scalars only). This task is the surface-syntax half; keep the two consistent — the
primitivekeyword marks exactly the set the compiler must know by name, which is the list plato-365 wants to shrink. - FunctionN declarations are generic:
primitive Function1<T0, TR>etc. (arity 0–9; Function0 has only a return type parameter).