step

package
v0.9.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 7 Imported by: 0

README

ifc/step — Go STEP/EXPRESS tokenizer + entity graph

Schema-agnostic STEP/SPF (ISO 10303-21) parser for IFC files, ported from ifcopenshell's parser + entity_instance model into idiomatic Go. Parses a .ifc file in-process into a navigable entity graph with forward and inverse references. No CAD kernel, no Python, no EXPRESS schema.

Scope: pure SPF, not the schema

A STEP file is purely positional — #5=IFCWALL('guid',#6,'name',...) stores attributes by position, never by name. This package exposes exactly what the raw stream yields; naming and type-hierarchy features are a separate schema layer built on top.

In scope (pure SPF, no schema) Out of scope (needs the EXPRESS schema)
attribute by indexinst.Get(i), inst.Args() attribute by nameinst.GlobalId
type keyword — inst.Type(), inst.IsA() (exact) is_a(supertype), ByType subtype expansion
forward refs — Value.Ref (resolved #id) named inverse attrs — .IsDecomposedBy
inverse graph — File.Inverse / InverseIndices / TotalInverses derived-attribute formulas
Traverse, ByID, ByType (exact), All by_guid, create_entity by name

IsA/ByType are exact-type only. Inverse exposes the raw referrer graph; projecting it into named IFC inverse attributes is the schema layer's job.

API

f, err := step.ParseFile("model.ifc")   // or ParseBytes([]byte) / Parse(io.Reader)
f.SchemaID()                             // "IFC2X3"
f.Len()                                  // instance count
wall, ok := f.ByID(42)                   // lookup by #id
walls := f.ByType("IfcWall")             // exact type (case-insensitive)
placement, ok := wall.Ref(5)             // resolved #id -> *Instance
referrers := f.Inverse(unit)             // who references this instance
closure := f.Traverse(project, step.Unbounded, step.DepthFirst) // forward closure
for inst := range f.All() { _ = inst }   // iterate all instances (no alloc)
f.Warnings()                             // non-fatal issues (e.g. dangling refs)

Grammar covered: #id refs, typed values (IFCLABEL(...)), enums (.MILLI.), booleans (.T./.F.) and logical unknown (.U., distinct from false), $ (unset) / * (derived), integers/reals (including non-conformant leading-dot reals like .5), binary, nested lists, complex instances (#id=(TYPEA(...)TYPEB(...))), and ISO-10303-21 string escapes (\X2\, \X4\, \X\, \S\, \P, ''). Tokenization is a character stream (not line-based), so multi-line records and /* */ comments parse correctly.

Design

Eager, two-pass, in-memory (ported from ifcopenshell's default in-memory variant):

ParseBytes(src)
  pass 1  scan HEADER + every #id=KEYWORD(args); record
          -> Instance{id, type, []Value}   (refs captured, unresolved)
          -> byID map, byType index, insertion order
  pass 2  walk every attribute
          -> resolve #ref -> *Instance (in place)
          -> build inverse index (target id -> []{referrer, attrIndex})
          -> dangling ref = non-fatal warning (ifcopenshell SYN 28 parity)

Measured — a 28 MB IFC2X3 ArchiCAD export

Metric Value
File size 29,558,941 B (~28 MB)
Instances 528,228
Entity types 93
Inverse edges 857,962
Parse time ~0.48–0.66 s (i7-14700K)
Peak heap ~306 MB
Allocations ~500 MB / 3.7 M allocs per parse

Memory driver: peak is dominated by the ~3.7 M Value structs (72 B each ≈ 266 MB, after field-order packing from 80 B), not string data — so string interning would not move peak. At ~30% of a 1 GB budget there's ample headroom; a columnar/arena Value rework is deferred behind this measurement and only worth it if a future input class blows the budget. Size worker limits against ~310 MB peak per 28 MB IFC, scaling roughly linearly with instance count.

Not in this package

Semantic model (IFCElement[]), geometry, quantities, and the EXPRESS schema layer are built on top of this package. This package stops at the navigable graph.

Documentation

Overview

Package step is a schema-agnostic STEP/SPF (ISO 10303-21) tokenizer and entity graph for IFC files, ported from ifcopenshell's parser and entity_instance model into idiomatic Go. It parses a .ifc file in-process into a navigable graph of entity instances with forward AND inverse references, using no CAD kernel, no Python, and no EXPRESS schema.

Scope: pure SPF, not the schema

A STEP file is purely positional: a record #5=IFCWALL('guid',#6,'name',...) stores attributes by position, never by name. This package exposes exactly what is recoverable from the raw stream; naming and type-hierarchy features are a separate schema layer built on top of it.

In scope (pure SPF, no schema)          Out of scope (needs the EXPRESS schema)
----------------------------            ---------------------------------------
attribute by INDEX  (inst.Get(i))       attribute by NAME  (inst.GlobalId)
type keyword        (inst.Type/IsA)     is_a(supertype), subtype expansion
forward refs        (Value.Ref)         named inverse attrs (.IsDecomposedBy)
inverse graph       (File.Inverse)      derived-attribute formulas
traverse, by-id, by-type (exact)        by_guid, create_entity by name

IsA and ByType are exact-type only. Inverse exposes the raw referrer graph; projecting it into named IFC inverse attributes is the schema layer's job.

Usage

f, err := step.ParseFile("model.ifc")
if err != nil { return err }
for _, wall := range f.ByType("IfcWall") {
	if placement, ok := wall.Ref(5); ok {
		_ = placement // #id resolved to *Instance
	}
}
// who references this unit?
for _, referrer := range f.Inverse(f.ByType("IfcSiUnit")[0]) { _ = referrer }

Parsing is eager and in-memory: the whole file is tokenized into instances in two passes (record load, then reference resolution + inverse indexing). Dangling references are non-fatal and surface via File.Warnings.

Index

Examples

Constants

View Source
const Unbounded = -1

Unbounded is the maxLevels value for an unbounded File.Traverse.

Variables

This section is empty.

Functions

This section is empty.

Types

type File

type File struct {
	Head Header
	// contains filtered or unexported fields
}

File is a parsed STEP model: the header plus a navigable entity graph. Lookups (ByID/ByType), the forward reference graph (resolved on Value.Ref), and the inverse index are all pure-SPF — no EXPRESS schema required.

func Parse

func Parse(r io.Reader) (*File, error)

Parse reads all of r and parses it as a STEP/SPF stream.

func ParseBytes

func ParseBytes(src []byte) (*File, error)

ParseBytes parses an in-memory STEP/SPF (ISO 10303-21) document into a navigable entity graph. It runs two passes: pass 1 tokenizes the HEADER and every DATA record into instances (references captured but unresolved) and builds the id and type indexes; pass 2 resolves references to instance pointers and builds the inverse index. Dangling references are non-fatal warnings.

Example
package main

import (
	"fmt"

	"github.com/blox-eng/goifc/step"
)

// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`

func main() {
	f, err := step.ParseBytes([]byte(sampleIFC))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("schema:", f.SchemaID())
	fmt.Println("instances:", f.Len())
}
Output:
schema: IFC4
instances: 7

func ParseFile

func ParseFile(path string) (*File, error)

ParseFile reads and parses a STEP/SPF file from path.

func (*File) All

func (f *File) All() iter.Seq[*Instance]

All returns an iterator over every instance in source (insertion) order. It allocates nothing and supports early termination:

for inst := range f.All() {
	...
}
Example
package main

import (
	"fmt"

	"github.com/blox-eng/goifc/step"
)

// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`

func main() {
	f, _ := step.ParseBytes([]byte(sampleIFC))
	count := 0
	for range f.All() { // no allocation; supports break
		count++
	}
	fmt.Println("total:", count)
}
Output:
total: 7

func (*File) ByID

func (f *File) ByID(id int) (*Instance, bool)

ByID looks up an instance by its STEP name (#id).

func (*File) ByType

func (f *File) ByType(keyword string) []*Instance

ByType returns all instances whose exact type keyword matches (case-insensitive). Subtype expansion (all IfcElement subtypes) requires the schema and is out of scope here.

Example
package main

import (
	"fmt"

	"github.com/blox-eng/goifc/step"
)

// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`

func main() {
	f, _ := step.ParseBytes([]byte(sampleIFC))
	for _, wall := range f.ByType("IfcWall") { // exact type, case-insensitive
		name, _ := wall.Get(2) // positional attribute (schema-agnostic)
		placement, _ := wall.Ref(5)
		fmt.Printf("#%d %s name=%q placement=#%d\n",
			wall.ID(), wall.Type(), name.Str, placement.ID())
	}
}
Output:
#10 IFCWALL name="Wall A" placement=#11

func (*File) Inverse

func (f *File) Inverse(inst *Instance) []*Instance

Inverse returns the distinct instances that reference inst via any forward attribute (the raw referrer set). Order follows first-seen referrer.

Example
package main

import (
	"fmt"

	"github.com/blox-eng/goifc/step"
)

// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`

func main() {
	f, _ := step.ParseBytes([]byte(sampleIFC))
	unit := f.ByType("IfcSiUnit")[0] // #3
	for _, referrer := range f.Inverse(unit) {
		fmt.Printf("#%d is referenced by #%d (%s)\n",
			unit.ID(), referrer.ID(), referrer.Type())
	}
}
Output:
#3 is referenced by #2 (IFCUNITASSIGNMENT)

func (*File) InverseIndices

func (f *File) InverseIndices(inst *Instance) []InverseRef

InverseIndices returns every (referrer, attribute-index) pair pointing at inst, including multiple entries for one referrer that references inst more than once. This is what a schema layer filters to build named inverse attributes.

func (*File) Len

func (f *File) Len() int

Len returns the number of DATA-section instances.

func (*File) SchemaID

func (f *File) SchemaID() string

SchemaID returns the first FILE_SCHEMA identifier (e.g. "IFC2X3"), or "".

func (*File) TotalInverses

func (f *File) TotalInverses(inst *Instance) int

TotalInverses returns the count of distinct instances referencing inst.

func (*File) Traverse

func (f *File) Traverse(inst *Instance, maxLevels int, order TraverseOrder) []*Instance

Traverse returns the forward transitive closure of inst (inst included), following resolved references through attributes, nested lists, and typed values. maxLevels is the depth limit: Unbounded (-1) for no limit, 0 for just inst. order selects DepthFirst or BreadthFirst. Each instance appears once.

Bounded traversal is depth-correct regardless of order: a node reachable within maxLevels is always included even if a longer path to it is explored first (bestDepth tracks the shortest known depth and allows a node to be re-expanded when a shorter path reaches it).

Example
package main

import (
	"fmt"
	"sort"

	"github.com/blox-eng/goifc/step"
)

// A tiny self-contained STEP/SPF document used by the examples.
const sampleIFC = `ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('demo.ifc','2026-07-21T00:00:00',(''),(''),'','demo','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1= IFCPROJECT('proj-guid',$,'Demo',$,$,$,$,$,#2);
#2= IFCUNITASSIGNMENT((#3));
#3= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#10= IFCWALL('wall-guid',$,'Wall A',$,$,#11,$,'tag');
#11= IFCLOCALPLACEMENT($,#12);
#12= IFCAXIS2PLACEMENT3D(#13,$,$);
#13= IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;`

func main() {
	f, _ := step.ParseBytes([]byte(sampleIFC))
	wall, _ := f.ByID(10)
	var ids []int
	for _, inst := range f.Traverse(wall, step.Unbounded, step.DepthFirst) {
		ids = append(ids, inst.ID())
	}
	sort.Ints(ids)
	fmt.Println("closure of #10:", ids)
}
Output:
closure of #10: [10 11 12 13]

func (*File) Warnings

func (f *File) Warnings() []string

Warnings returns non-fatal issues encountered during parse (e.g. dangling references to missing instances), mirroring ifcopenshell's SYN diagnostics.

type Header struct {
	Description         []string // FILE_DESCRIPTION descriptions
	ImplementationLevel string   // FILE_DESCRIPTION implementation_level
	Name                []string // FILE_NAME fields (raw, in declaration order)
	Schema              []string // FILE_SCHEMA identifiers, e.g. ["IFC2X3"]
}

Header holds the ISO-10303-21 HEADER section fields.

type Instance

type Instance struct {
	// contains filtered or unexported fields
}

Instance is a parsed STEP entity instance: a schema-agnostic entity_instance (ported from ifcopenshell). Attribute access is positional — access by NAME is a schema concern layered on by a later component. The type keyword and argument list come straight from the SPF record.

func (*Instance) Args

func (i *Instance) Args() []Value

Args returns the underlying attribute slice. Callers must not mutate it.

func (*Instance) File

func (i *Instance) File() *File

File returns the owning file.

func (*Instance) Get

func (i *Instance) Get(idx int) (Value, bool)

Get returns the attribute at idx and whether idx is in range.

func (*Instance) ID

func (i *Instance) ID() int

ID returns the STEP instance name (#id). The zero-value Instance reports 0.

func (*Instance) IsA

func (i *Instance) IsA(keyword string) bool

IsA reports whether this instance's exact type keyword equals keyword (case-insensitive). For a complex instance it matches any of its part types. This is exact-only: supertype checks (IfcWall IS-A IfcElement) require the EXPRESS schema and are out of scope here.

func (*Instance) Len

func (i *Instance) Len() int

Len returns the number of positional attributes.

func (*Instance) Ref

func (i *Instance) Ref(idx int) (*Instance, bool)

Ref returns the resolved instance referenced by attribute idx, if that attribute is a resolved reference (#id). ok is false when idx is out of range, not a ref, or a dangling ref whose target was missing.

func (*Instance) Type

func (i *Instance) Type() string

Type returns the upper-cased entity type keyword. It is the schema-agnostic equivalent of ifcopenshell's is_a() with no arguments — an exact type string, not a supertype-chain check.

func (*Instance) Walk

func (i *Instance) Walk(fn func(Value))

Walk applies fn to every value in every attribute, pre-order (nested lists and typed values included).

type InverseRef

type InverseRef struct {
	From      *Instance
	AttrIndex int
}

InverseRef records one referrer of an instance: the referring instance and the top-level attribute index on it that holds the reference. This is the raw referrer graph; projecting it into named IFC inverse attributes is a schema concern for a later component.

type Kind

type Kind uint8

Kind tags the variant of a parsed STEP attribute value. It mirrors the runtime categories ifcopenshell distinguishes from the SPF token alone (no schema): the declared EXPRESS type is a separate, schema-driven concern layered on later.

const (
	KindNull    Kind = iota // $  (unset / omitted optional)
	KindDerived             // *  (value derived in a supertype)
	KindInt                 // integer literal        -> I
	KindFloat               // real literal           -> F
	KindString              // '...' (decoded)        -> Str
	KindEnum                // .LABEL.                -> Str (label, no dots)
	KindBool                // .T./.F.                -> B (EXPRESS BOOLEAN)
	KindLogical             // .U.                    -> (no payload) EXPRESS LOGICAL "unknown", distinct from false
	KindBinary              // "0..." binary          -> Str (raw hex/bit text)
	KindRef                 // #id                    -> RefID, Ref (nil until resolved)
	KindList                // (...) aggregate        -> List
	KindTyped               // KEYWORD(inner)         -> Str (keyword) + List (inner args)
)

func (Kind) String

func (k Kind) String() string

String returns the kind name, so kinds render readably in error messages.

type ParseError

type ParseError struct {
	Offset int   // byte offset into the source where the scanner stopped
	Err    error // the underlying syntax error
}

ParseError reports a STEP/SPF syntax error together with the byte offset into the source where parsing stopped. Callers can match it with errors.As:

f, err := step.ParseBytes(src)
var pe *step.ParseError
if errors.As(err, &pe) {
	log.Printf("bad IFC at byte %d: %v", pe.Offset, pe.Err)
}

I/O errors from ParseFile (e.g. a missing file) are returned unwrapped, so errors.Is(err, os.ErrNotExist) still works — only syntax errors are ParseErrors.

func (*ParseError) Error

func (e *ParseError) Error() string

func (*ParseError) Unwrap

func (e *ParseError) Unwrap() error

type Scanner

type Scanner struct {
	// contains filtered or unexported fields
}

Scanner is a character-stream lexer over an in-memory STEP source buffer. It is deliberately not line-based: STEP permits a single entity record to span many lines, so tokenization walks the byte stream directly.

func NewScanner

func NewScanner(src []byte) *Scanner

NewScanner returns a Scanner over src. src is retained (not copied); Token.Text values alias it, so callers must not mutate src while scanning.

func (*Scanner) Next

func (s *Scanner) Next() Token

Next returns the next token, or a Token with Kind==TokEOF once the input is exhausted (repeatedly). It never panics on malformed input; an unterminated string or binary literal yields a token up to end-of-input for the parser layer to reject.

func (*Scanner) Pos

func (s *Scanner) Pos() int

Pos reports the scanner's current byte offset into the source, used to attach positions to parse errors.

type Token

type Token struct {
	Kind TokenKind
	Text []byte
}

Token is one lexical unit. Text is a sub-slice of the source buffer (zero-copy); it is only meaningful for the value-bearing kinds (Ref, String, Enum, Bool, Binary, Int, Float, Keyword) — for operators it holds the operator byte(s).

type TokenKind

type TokenKind uint8

TokenKind classifies a STEP/SPF (ISO 10303-21) lexical token. The set mirrors ifcopenshell's IfcSpfLexer token types, ported to Go.

const (
	TokEOF     TokenKind = iota
	TokLParen            // (
	TokRParen            // )
	TokComma             // ,
	TokEquals            // =
	TokSemi              // ;
	TokDollar            // $  unset / null
	TokStar              // *  derived-in-supertype placeholder
	TokRef               // #123  (Text excludes the '#')
	TokString            // '...' (Text = raw bytes between the quotes, still escaped)
	TokEnum              // .FOO. (Text = label without the dots)
	TokBool              // .T. / .F. / .U. (Text = T/F/U)
	TokBinary            // "0..." (Text = bytes between the quotes)
	TokInt               // 123 / -4 / +7
	TokFloat             // 1. / -2.5E-3
	TokKeyword           // IFCWALL, ISO-10303-21, HEADER, DATA, ENDSEC ...
)

func (TokenKind) String

func (k TokenKind) String() string

String returns the token kind name, so kinds render readably in error messages.

type TraverseOrder

type TraverseOrder int

TraverseOrder selects the visitation order of File.Traverse.

const (
	DepthFirst   TraverseOrder = iota // pre-order depth-first (default)
	BreadthFirst                      // level-order breadth-first
)

type Value

type Value struct {
	Str   string    // KindString / KindEnum / KindBinary / KindTyped(keyword)
	List  []Value   // KindList / KindTyped(inner args)
	Ref   *Instance // KindRef (resolved target; nil if the target is missing)
	F     float64   // KindFloat
	I     int64     // KindInt
	RefID uint32    // KindRef (target id, pre-resolution)
	Kind  Kind      // variant tag
	B     bool      // KindBool (.T. -> true, .F. -> false); .U. is KindLogical, not this field
}

Value is a parsed STEP attribute value: an eager, in-memory tagged union (ported from ifcopenshell's attribute-value variant). It is a plain struct, not a boxed interface, so the millions of values in a large model avoid per-value heap allocation. Only the field(s) named for a Kind carry meaning.

Fields are ordered largest-alignment-first so the struct packs to 72 bytes (vs 80 with a naive layout) — a ~10% cut on the dominant memory cost of a big model.

func (Value) Walk

func (v Value) Walk(fn func(Value))

Walk applies fn to v and, pre-order, to every value nested within it (lists and typed-value inner args). It operates on value copies — to mutate stored values (e.g. resolving refs) the parser uses an internal by-pointer walk instead.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL