FCS Essentials
FCS is a declarative language for HiStruct/FemCAD models. This page is intentionally conservative: examples here should be valid FCS patterns, not pseudocode inspired by other languages.
If a snippet has not been checked with FLI or copied from a tested gist, do not promote it as working syntax.
The Most Important Boundary
FCS has both expressions and model-building statements. Do not blur them.
gblock is a statement-like model declaration. It is not an expression you can return from a lambda, assign into a list, or generate inside .Select(...).
Do not write gblock inside .Select(...), a lambda body, or a top-level assignment as if it returns a value. Use explicit named gblock declarations, or use a verified distribution pattern when you need repeated placement.
How FCS Evaluates
FCS is a pure functional language. A file is a set of definitions, like a spreadsheet written as source code.
- Nothing runs top to bottom. A client (FLI, the VS Code F5 command, the HiStruct server) asks for the value of one expression, and the engine pulls in only the definitions that expression depends on. The order of definitions in the file does not matter.
- Definitions that nothing reads are never evaluated, so a broken but unused definition does not fail.
- There are no side effects: no print statement, no I/O, no mutation. Values are immutable. Defining the same name twice is an error under the E4 engines;
e3.emulationsilently keeps the last definition. - The same inputs and the same source always give the same result. HiStruct relies on this to cache results by
hash(source commit) + hash(inputs). See Platform Architecture.
Every .fcs file is also a class. A sibling file quadsol.fcs can be used as quadsol{a = 1, b = -3, c = 2}.resultArray without a declaration, or declared explicitly with gclass {gcQuad} filename "quadsol.fcs". Class{ ... } is an updater: it returns a copy of the instance with the listed fields replaced. Updaters compose: Q = quadsol{c = -10} then Q{a = 1, b = 2}.
:= vs =
Both operators are valid at the top level and inside updaters (Class{ ... }, parameters { ... }). Neither is "the top-level one" or "the parameter one". The difference is whether a value is an input that callers may override, and when it is evaluated.
The rule of thumb from the engine author: = says "nobody overrides this, you may inline and optimise it"; := says "this may be an input, keep a slot for it".
Top-level fields of a class file
a := 1 # input: lazy, cached, overridable by callers / PVC
b = 2 # internal value
s = a + b
Overriding each field from another file with Class{a = 10}.s and Class{b = 20}.s:
Engine (.fcsconfig.json) |
Class{a = 10}.s |
Class{b = 20}.s |
|---|---|---|
e3.emulation (no config file) |
12 | 21 |
e4.trans |
12 | 21 |
e4.pure |
12 | error: Unable to update readonly field 'b' ... define it with ':=' |
Declare every value that a caller, a parent gblock or user input (PVC) may change with :=. Use = for derived or internal values. Code written this way works under every engine.
Inside updaters and parameters { ... }
x = gc{ a = someValue }.t # '=' entry
y = gc{ a := someValue }.t # ':=' entry
p := exprkeeps the expression (with the caller's scope) and evaluates it only when the child readsp.p = exprpasses a value. Undere4.transande4.pure, an updater whose entries are all=is evaluated eagerly.expris computed even if the child never readsp, so a failingexprfails the whole expression. Undere3.emulationupdaters are lazy either way.
For constants and pass-through values (parameters {w = w}) both forms give the same result. Prefer := when the value is expensive or might fail.
Verified with fli.exe on 2026-10-12. See also Legacy engine warning and .fcsconfig.json.
Geometry Declarations
Basic geometry is declared with geometry keywords, not with invented helper calls:
vertex {p} xyz 1 2 3
That is the source of the Golden Example: Vertex. Evaluating p.X should return:
1
When coordinate arguments contain expressions, keep the expression syntax from verified examples. See Geometry Primitives and the tested geometry gists before extrapolating.
Reuse With gclass And gblock
A common reuse pattern is:
gclass {gcTable} filename "Table.fcs"
gclass {gcChair} filename "Chair.fcs"
gblock {gbTable} gclass {gcTable} lcs (GCS) parameters {tableWidth=tableWidth, tableLength=tableLength, tableHeight=tableHeight}
gblock {gbSnowWhite} gclass {gcChair} lcs (GCS.Ty(swY).Rz(PI)) parameters {seatHeight=swSeatH, seatWidth=swSeatW, seatDepth=swSeatD, backHeight=swBackH}
This is copied from the checked Snow White Dining example. Notice that each block has a stable name. The page does not pretend that gblock can be produced from .Select.
Lists And Lambdas
Use .Select(...) for values, numbers, records, or data needed by later declarations:
numbers := [1, 2, 3, 4, 5]
doubled := numbers.Select(x => x * 2)
Do not use .Select(...) to emit vertex, curve, area, volume, gclass, or gblock declarations unless that exact construct is backed by a working FLI test.
Repetition
For repeated model elements, prefer one of these documented routes:
- explicit named
gblockstatements for small fixed counts, distributionpatterns from Configurator Patterns,- tested gist examples such as GBlock Placement and Distribution Linear Array.
Do not invent collection-comprehension syntax for model statements.