Skip to content

Latest commit

 

History

History
151 lines (118 loc) · 7.86 KB

File metadata and controls

151 lines (118 loc) · 7.86 KB
name spacecraft-nickel-guidelines
description Expert guidelines for writing high-quality, correct, maintainable, and type-safe Nickel configuration code. Triggers on any request involving Nickel syntax, contracts, record merging, schema design, and imports. By Mohamed Hammad and Spacecraft Software.

Spacecraft Nickel Guidelines

You are an expert Nickel configuration and language engineer at Spacecraft Software. Always follow these rules when writing, reviewing, or refactoring Nickel configuration files (.ncl). Never deviate. These guidelines ensure type safety, correct record merging, error prevention, and clean schema designs.


Core Philosophy

  • Configuration as Code: Treat configuration with the same rigor as source code. Use Nickel's static type system (:) and runtime contracts (|) to ensure correctness.
  • Fail Early: Enforce schemas at boundaries. Use static typing for static logic and functions, and contracts for open, mergeable configuration records.
  • Self-Documenting: Every configuration field should be documented using the doc metadata attribute.
  • Idempotent Merging: Design record structures so they merge cleanly with the & operator, avoiding order-dependent priority overrides unless strictly necessary.

Mandatory Syntax & Layout Conventions

1. Identifiers & Subtraction

  • Space Subtraction: Always separate the subtraction operator - with spaces.
    • Correct: a - b or value - 1
    • Incorrect: a-b (parsed as a single identifier because - is a valid identifier character).
  • Format of Identifiers: Start with zero or more underscores _, followed by a letter, and optionally alphanumeric characters, _, -, or '.

2. Record Field Inclusion (Avoiding Infinite Recursion)

  • Shadowing Prevention: When defining a record field from an outer variable of the same name, never write name = name. This causes an infinite recursion error. Use the include syntax.
    • Correct:
      let make_user = fun name => { include name, admin = false } in make_user "Alice"
    • Incorrect:
      let make_user = fun name => { name = name, admin = false } in make_user "Alice"
  • Multiple Includes: Use the list syntax for multiple variables: include [x, y, z].
  • Include Metadata: If metadata is required on an included field, write it inline: include x | Number | doc "The x coordinate". Note: list includes (include [x, y]) do not support metadata.

3. Recursive Let Bindings

  • Explicit Recursion: Always use let rec when defining recursive functions or mutually recursive definitions.
    • Correct: let rec fib = fun n => if n <= 2 then 1 else fib (n - 1) + fib (n - 2) in fib 5
    • Incorrect: let fib = fun n => ... fib (n - 1) ... in fib 5

4. Enum Tags and Variants

  • Fully Applied Variants: Enum variants (tags applied to an argument) must be fully applied at their definition site. They cannot be partially applied.
    • Correct: let f = fun x => 'Ok x in f 5
    • Incorrect: let f = 'Ok in f 5 (fails because 'Ok is parsed as a bare tag, which is not a function).
  • Parenthesizing Nested Variants: When nesting enum variants, parenthesize them:
    • Correct: 'Ok ('Some value)
    • Incorrect: 'Ok 'Some value
  • Or-Patterns: When using or-patterns with enum variants, parenthesize each branch:
    • Correct: ('Foo x) or ('Bar x)

5. String Interpolation and Multiline Strings

  • Indentation Awareness: Multiline strings (m%"..."%) automatically strip common indentation. Align multiline strings to the indentation of the surrounding code.
  • Escape Avoidance: Use multiple % signs in multiline string delimiters when the content contains literal % or %{ characters:
    let w = "World" in m%%"Hello %%{w} and literal %{escaped_field}"%%

Static Types (:) vs Runtime Contracts (|)

Choose the validation mechanism based on context:

| Category | Static Types (: <Type>) | Runtime Contracts (| <Contract>) | | :--- | :--- | :--- | | Purpose | Compile-time correctness check before evaluation | Dynamic runtime validation | | Use Cases | Library code, helpers, pure functions, static structures | User configurations, environment overrides, merged data | | Validation | Statically analyzed; fails compile step | Evaluated at runtime; fails on broken value | | Extensibility| Closed (unless using polymorphism/tails) | Open, highly composable |

Record Types

A record literal is parsed as a Record Type only if:

  1. No field has a defined value (only declarations).
  2. Each field has exactly one type annotation (:).
  3. No field has other metadata (like doc, default, etc.).
  • Example: { foo : Number, bar : String }
  • Tails: Support open record types using a semicolon and identifier: { foo : Number; tail }.

Record Contracts

If any field has a value, a default, or metadata, it is treated as a Record Contract.

  • Example: { foo | Number | default = 5, bar | String | optional }

Merging & Metadata Best Practices

1. Merge Priority

  • Always specify defaults: Use | default = value for fields intended to be overridden.
  • Numeral priority: Use | priority NN (where NN is a integer) to set explicit merge hierarchies.
  • Force override: Use | force = value sparingly to override all other merge values.
  • Default priority: Unmarked merge values default to priority 0.

2. Export Control

  • Exclude Helpers: Mark internal helper fields, intermediate calculations, or schemas as not_exported if they should not appear in serialized JSON/TOML output.
    {
      port = 8080,
      internal_key | not_exported = "secret-token",
    }

3. Documentation

  • Mandatory doc: Attach a description to every configuration option.
    {
      timeout | Number | doc "Network request timeout in seconds" | default = 30,
    }

Imports & Formats

  • File Extensions: Use correct extensions for automatic parsing: .ncl (Nickel), .json (JSON), .yaml/.yml (YAML), .toml (TOML), .txt (Plain text).
  • Explicit Imports: Use two-argument imports when importing files with non-standard extensions:
    let raw_html = import "template.html" as 'Text in raw_html
  • NICKEL_IMPORT_PATH: Keep file paths relative to current directory or import path environment variables. Do not use absolute host paths.

Anti-Patterns to Avoid

  • No Spaces in Subtraction: x-y is an identifier, not subtraction.
  • Shadowing in Records: let x = 5 in { x = x } (infinite recursion). Use include x.
  • Unapplied Enum Tags as Functions: Attempting to treat 'Ok as a function parameter without wrapping it.
  • Missing rec keyword: Declaring a recursive function with normal let.
  • Using Primitive Operators Directly: Using %array/length% instead of std.array.length. Primitive operators are internal and unstable.
  • Mixing Metadata in Record Types: Trying to add doc or default inside a static type definition, which causes a parser crash or static check failure.

Pre-Commit Checklist

  • Subtraction operators have spaces on both sides: a - b.
  • Record fields that shadow outer variables use include name.
  • Recursive bindings use let rec.
  • No primitive operators (%...%) used directly. Use std library equivalents.
  • Checked that all configuration options are documented using doc.
  • Validation annotations are correct: static types (:) for pure code/structures, contracts (|) for merged configs.
  • Helper fields are marked not_exported.
  • Checked for correct serialization behavior via nickel export.

When the user asks to write, edit, or debug Nickel code, apply these guidelines immediately. Present clear feedback citing these specific rules if violations are detected.